/*!
 * Copyright 2015 Drifty Co.
 * http://drifty.com/
 *
 * Ionic, v1.3.4
 * A powerful HTML5 mobile app framework.
 * http://ionicframework.com/
 *
 * By @maxlynch, @benjsperry, @adamdbradley <3
 *
 * Licensed under the MIT license. Please see LICENSE for more information.
 *
 */

(function () {
  /* eslint no-unused-vars:0 */
  var IonicModule = angular.module('ionic', ['ngAnimate', 'ngSanitize', 'ui.router', 'ngIOS9UIWebViewPatch']),
    extend = angular.extend,
    forEach = angular.forEach,
    isDefined = angular.isDefined,
    isNumber = angular.isNumber,
    isString = angular.isString,
    jqLite = angular.element,
    noop = angular.noop;

  /**
   * @ngdoc service
   * @name $ionicActionSheet
   * @module ionic
   * @description
   * The Action Sheet is a slide-up pane that lets the user choose from a set of options.
   * Dangerous options are highlighted in red and made obvious.
   *
   * There are easy ways to cancel out of the action sheet, such as tapping the backdrop or even
   * hitting escape on the keyboard for desktop testing.
   *
   * ![Action Sheet](http://ionicframework.com.s3.amazonaws.com/docs/controllers/actionSheet.gif)
   *
   * @usage
   * To trigger an Action Sheet in your code, use the $ionicActionSheet service in your angular controllers:
   *
   * ```js
   * angular.module('mySuperApp', ['ionic'])
   * .controller(function($scope, $ionicActionSheet, $timeout) {
   *
   *  // Triggered on a button click, or some other target
   *  $scope.show = function() {
   *
   *    // Show the action sheet
   *    var hideSheet = $ionicActionSheet.show({
   *      buttons: [
   *        { text: '<b>Share</b> This' },
   *        { text: 'Move' }
   *      ],
   *      destructiveText: 'Delete',
   *      titleText: 'Modify your album',
   *      cancelText: 'Cancel',
   *      cancel: function() {
          // add cancel code..
        },
   *      buttonClicked: function(index) {
   *        return true;
   *      }
   *    });
   *
   *    // For example's sake, hide the sheet after two seconds
   *    $timeout(function() {
   *      hideSheet();
   *    }, 2000);
   *
   *  };
   * });
   * ```
   *
   */
  IonicModule
  .factory('$ionicActionSheet', [
    '$rootScope',
    '$compile',
    '$animate',
    '$timeout',
    '$ionicTemplateLoader',
    '$ionicPlatform',
    '$ionicBody',
    'IONIC_BACK_PRIORITY',
    function ($rootScope, $compile, $animate, $timeout, $ionicTemplateLoader, $ionicPlatform, $ionicBody, IONIC_BACK_PRIORITY) {

      return {
        show: actionSheet
      };

      /**
       * @ngdoc method
       * @name $ionicActionSheet#show
       * @description
       * Load and return a new action sheet.
       *
       * A new isolated scope will be created for the
       * action sheet and the new element will be appended into the body.
       *
       * @param {object} options The options for this ActionSheet. Properties:
       *
       *  - `[Object]` `buttons` Which buttons to show.  Each button is an object with a `text` field.
       *  - `{string}` `titleText` The title to show on the action sheet.
       *  - `{string=}` `cancelText` the text for a 'cancel' button on the action sheet.
       *  - `{string=}` `destructiveText` The text for a 'danger' on the action sheet.
       *  - `{function=}` `cancel` Called if the cancel button is pressed, the backdrop is tapped or
       *     the hardware back button is pressed.
       *  - `{function=}` `buttonClicked` Called when one of the non-destructive buttons is clicked,
       *     with the index of the button that was clicked and the button object. Return true to close
       *     the action sheet, or false to keep it opened.
       *  - `{function=}` `destructiveButtonClicked` Called when the destructive button is clicked.
       *     Return true to close the action sheet, or false to keep it opened.
       *  -  `{boolean=}` `cancelOnStateChange` Whether to cancel the actionSheet when navigating
       *     to a new state.  Default true.
       *  - `{string}` `cssClass` The custom CSS class name.
       *
       * @returns {function} `hideSheet` A function which, when called, hides & cancels the action sheet.
       */
      function actionSheet(opts) {
        var scope = $rootScope.$new(true);

        extend(scope, {
          cancel: noop,
          destructiveButtonClicked: noop,
          buttonClicked: noop,
          $deregisterBackButton: noop,
          buttons: [],
          cancelOnStateChange: true
        }, opts || {});

        function textForIcon(text) {
          if (text && /icon/.test(text)) {
            scope.$actionSheetHasIcon = true;
          }
        }

        for (var x = 0; x < scope.buttons.length; x++) {
          textForIcon(scope.buttons[x].text);
        }
        textForIcon(scope.cancelText);
        textForIcon(scope.destructiveText);

        // Compile the template
        var element = scope.element = $compile('<ion-action-sheet ng-class="cssClass" buttons="buttons"></ion-action-sheet>')(scope);

        // Grab the sheet element for animation
        var sheetEl = jqLite(element[0].querySelector('.action-sheet-wrapper'));

        var stateChangeListenDone = scope.cancelOnStateChange ?
          $rootScope.$on('$stateChangeSuccess', function () {
            scope.cancel();
          }) :
          noop;

        // removes the actionSheet from the screen
        scope.removeSheet = function (done) {
          if (scope.removed) return;

          scope.removed = true;
          sheetEl.removeClass('action-sheet-up');
          $timeout(function () {
            // wait to remove this due to a 300ms delay native
            // click which would trigging whatever was underneath this
            $ionicBody.removeClass('action-sheet-open');
          }, 400);
          scope.$deregisterBackButton();
          stateChangeListenDone();

          $animate.removeClass(element, 'active').then(function () {
            scope.$destroy();
            element.remove();
            // scope.cancel.$scope is defined near the bottom
            scope.cancel.$scope = sheetEl = null;
            (done || noop)(opts.buttons);
          });
        };

        scope.showSheet = function (done) {
          if (scope.removed) return;

          $ionicBody.append(element)
          .addClass('action-sheet-open');

          $animate.addClass(element, 'active').then(function () {
            if (scope.removed) return;
            (done || noop)();
          });
          $timeout(function () {
            if (scope.removed) return;
            sheetEl.addClass('action-sheet-up');
          }, 20, false);
        };

        // registerBackButtonAction returns a callback to deregister the action
        scope.$deregisterBackButton = $ionicPlatform.registerBackButtonAction(
          function () {
            $timeout(scope.cancel);
          },
          IONIC_BACK_PRIORITY.actionSheet
        );

        // called when the user presses the cancel button
        scope.cancel = function () {
          // after the animation is out, call the cancel callback
          scope.removeSheet(opts.cancel);
        };

        scope.buttonClicked = function (index) {
          // Check if the button click event returned true, which means
          // we can close the action sheet
          if (opts.buttonClicked(index, opts.buttons[index]) === true) {
            scope.removeSheet();
          }
        };

        scope.destructiveButtonClicked = function () {
          // Check if the destructive button click event returned true, which means
          // we can close the action sheet
          if (opts.destructiveButtonClicked() === true) {
            scope.removeSheet();
          }
        };

        scope.showSheet();

        // Expose the scope on $ionicActionSheet's return value for the sake
        // of testing it.
        scope.cancel.$scope = scope;

        return scope.cancel;
      }
    }]);


  jqLite.prototype.addClass = function (cssClasses) {
    var x, y, cssClass, el, splitClasses, existingClasses;
    if (cssClasses && cssClasses != 'ng-scope' && cssClasses != 'ng-isolate-scope') {
      for (x = 0; x < this.length; x++) {
        el = this[x];
        if (el.setAttribute) {

          if (cssClasses.indexOf(' ') < 0 && el.classList.add) {
            el.classList.add(cssClasses);
          } else {
            existingClasses = (' ' + (el.getAttribute('class') || '') + ' ')
            .replace(/[\n\t]/g, " ");
            splitClasses = cssClasses.split(' ');

            for (y = 0; y < splitClasses.length; y++) {
              cssClass = splitClasses[y].trim();
              if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) {
                existingClasses += cssClass + ' ';
              }
            }
            el.setAttribute('class', existingClasses.trim());
          }
        }
      }
    }
    return this;
  };

  jqLite.prototype.removeClass = function (cssClasses) {
    var x, y, splitClasses, cssClass, el;
    if (cssClasses) {
      for (x = 0; x < this.length; x++) {
        el = this[x];
        if (el.getAttribute) {
          if (cssClasses.indexOf(' ') < 0 && el.classList.remove) {
            el.classList.remove(cssClasses);
          } else {
            splitClasses = cssClasses.split(' ');

            for (y = 0; y < splitClasses.length; y++) {
              cssClass = splitClasses[y];
              el.setAttribute('class', (
                (" " + (el.getAttribute('class') || '') + " ")
                .replace(/[\n\t]/g, " ")
                .replace(" " + cssClass.trim() + " ", " ")).trim()
              );
            }
          }
        }
      }
    }
    return this;
  };

  /**
   * @ngdoc service
   * @name $ionicBackdrop
   * @module ionic
   * @description
   * Shows and hides a backdrop over the UI.  Appears behind popups, loading,
   * and other overlays.
   *
   * Often, multiple UI components require a backdrop, but only one backdrop is
   * ever needed in the DOM at a time.
   *
   * Therefore, each component that requires the backdrop to be shown calls
   * `$ionicBackdrop.retain()` when it wants the backdrop, then `$ionicBackdrop.release()`
   * when it is done with the backdrop.
   *
   * For each time `retain` is called, the backdrop will be shown until `release` is called.
   *
   * For example, if `retain` is called three times, the backdrop will be shown until `release`
   * is called three times.
   *
   * **Notes:**
   * - The backdrop service will broadcast 'backdrop.shown' and 'backdrop.hidden' events from the root scope,
   * this is useful for alerting native components not in html.
   *
   * @usage
   *
   * ```js
   * function MyController($scope, $ionicBackdrop, $timeout, $rootScope) {
   *   //Show a backdrop for one second
   *   $scope.action = function() {
   *     $ionicBackdrop.retain();
   *     $timeout(function() {
   *       $ionicBackdrop.release();
   *     }, 1000);
   *   };
   *
   *   // Execute action on backdrop disappearing
   *   $scope.$on('backdrop.hidden', function() {
   *     // Execute action
   *   });
   *
   *   // Execute action on backdrop appearing
   *   $scope.$on('backdrop.shown', function() {
   *     // Execute action
   *   });
   *
   * }
   * ```
   */
  IonicModule
  .factory('$ionicBackdrop', [
    '$document', '$timeout', '$$rAF', '$rootScope',
    function ($document, $timeout, $$rAF, $rootScope) {

      var el = jqLite('<div class="backdrop">');
      var backdropHolds = 0;

      $document[0].body.appendChild(el[0]);

      return {
        /**
         * @ngdoc method
         * @name $ionicBackdrop#retain
         * @description Retains the backdrop.
         */
        retain: retain,
        /**
         * @ngdoc method
         * @name $ionicBackdrop#release
         * @description
         * Releases the backdrop.
         */
        release: release,

        getElement: getElement,

        // exposed for testing
        _element: el
      };

      function retain() {
        backdropHolds++;
        if (backdropHolds === 1) {
          el.addClass('visible');
          $rootScope.$broadcast('backdrop.shown');
          $$rAF(function () {
            // If we're still at >0 backdropHolds after async...
            if (backdropHolds >= 1) el.addClass('active');
          });
        }
      }

      function release() {
        if (backdropHolds === 1) {
          el.removeClass('active');
          $rootScope.$broadcast('backdrop.hidden');
          $timeout(function () {
            // If we're still at 0 backdropHolds after async...
            if (backdropHolds === 0) el.removeClass('visible');
          }, 400, false);
        }
        backdropHolds = Math.max(0, backdropHolds - 1);
      }

      function getElement() {
        return el;
      }

    }]);

  /**
   * @private
   */
  IonicModule
  .factory('$ionicBind', ['$parse', '$interpolate', function ($parse, $interpolate) {
    var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
    return function (scope, attrs, bindDefinition) {
      forEach(bindDefinition || {}, function (definition, scopeName) {
        //Adapted from angular.js $compile
        var match = definition.match(LOCAL_REGEXP) || [],
          attrName = match[3] || scopeName,
          mode = match[1], // @, =, or &
          parentGet,
          unwatch;

        switch (mode) {
          case '@':
            if (!attrs[attrName]) {
              return;
            }
            attrs.$observe(attrName, function (value) {
              scope[scopeName] = value;
            });
            // we trigger an interpolation to ensure
            // the value is there for use immediately
            if (attrs[attrName]) {
              scope[scopeName] = $interpolate(attrs[attrName])(scope);
            }
            break;

          case '=':
            if (!attrs[attrName]) {
              return;
            }
            unwatch = scope.$watch(attrs[attrName], function (value) {
              scope[scopeName] = value;
            });
            //Destroy parent scope watcher when this scope is destroyed
            scope.$on('$destroy', unwatch);
            break;

          case '&':
            /* jshint -W044 */
            if (attrs[attrName] && attrs[attrName].match(RegExp(scopeName + '\(.*?\)'))) {
              throw new Error('& expression binding "' + scopeName + '" looks like it will recursively call "' +
                attrs[attrName] + '" and cause a stack overflow! Please choose a different scopeName.');
            }
            parentGet = $parse(attrs[attrName]);
            scope[scopeName] = function (locals) {
              return parentGet(scope, locals);
            };
            break;
        }
      });
    };
  }]);

  /**
   * @ngdoc service
   * @name $ionicBody
   * @module ionic
   * @description An angular utility service to easily and efficiently
   * add and remove CSS classes from the document's body element.
   */
  IonicModule
  .factory('$ionicBody', ['$document', function ($document) {
    return {
      /**
       * @ngdoc method
       * @name $ionicBody#addClass
       * @description Add a class to the document's body element.
       * @param {string} class Each argument will be added to the body element.
       * @returns {$ionicBody} The $ionicBody service so methods can be chained.
       */
      addClass: function () {
        for (var x = 0; x < arguments.length; x++) {
          $document[0].body.classList.add(arguments[x]);
        }
        return this;
      },
      /**
       * @ngdoc method
       * @name $ionicBody#removeClass
       * @description Remove a class from the document's body element.
       * @param {string} class Each argument will be removed from the body element.
       * @returns {$ionicBody} The $ionicBody service so methods can be chained.
       */
      removeClass: function () {
        for (var x = 0; x < arguments.length; x++) {
          $document[0].body.classList.remove(arguments[x]);
        }
        return this;
      },
      /**
       * @ngdoc method
       * @name $ionicBody#enableClass
       * @description Similar to the `add` method, except the first parameter accepts a boolean
       * value determining if the class should be added or removed. Rather than writing user code,
       * such as "if true then add the class, else then remove the class", this method can be
       * given a true or false value which reduces redundant code.
       * @param {boolean} shouldEnableClass A true/false value if the class should be added or removed.
       * @param {string} class Each remaining argument would be added or removed depending on
       * the first argument.
       * @returns {$ionicBody} The $ionicBody service so methods can be chained.
       */
      enableClass: function (shouldEnableClass) {
        var args = Array.prototype.slice.call(arguments).slice(1);
        if (shouldEnableClass) {
          this.addClass.apply(this, args);
        } else {
          this.removeClass.apply(this, args);
        }
        return this;
      },
      /**
       * @ngdoc method
       * @name $ionicBody#append
       * @description Append a child to the document's body.
       * @param {element} element The element to be appended to the body. The passed in element
       * can be either a jqLite element, or a DOM element.
       * @returns {$ionicBody} The $ionicBody service so methods can be chained.
       */
      append: function (ele) {
        $document[0].body.appendChild(ele.length ? ele[0] : ele);
        return this;
      },
      /**
       * @ngdoc method
       * @name $ionicBody#get
       * @description Get the document's body element.
       * @returns {element} Returns the document's body element.
       */
      get: function () {
        return $document[0].body;
      }
    };
  }]);

  IonicModule
  .factory('$ionicClickBlock', [
    '$document',
    '$ionicBody',
    '$timeout',
    function ($document, $ionicBody, $timeout) {
      var CSS_HIDE = 'click-block-hide';
      var cbEle, fallbackTimer, pendingShow;

      function preventClick(ev) {
        ev.preventDefault();
        ev.stopPropagation();
      }

      function addClickBlock() {
        if (pendingShow) {
          if (cbEle) {
            cbEle.classList.remove(CSS_HIDE);
          } else {
            cbEle = $document[0].createElement('div');
            cbEle.className = 'click-block';
            $ionicBody.append(cbEle);
            cbEle.addEventListener('touchstart', preventClick);
            cbEle.addEventListener('mousedown', preventClick);
          }
          pendingShow = false;
        }
      }

      function removeClickBlock() {
        cbEle && cbEle.classList.add(CSS_HIDE);
      }

      return {
        show: function (autoExpire) {
          pendingShow = true;
          $timeout.cancel(fallbackTimer);
          fallbackTimer = $timeout(this.hide, autoExpire || 310, false);
          addClickBlock();
        },
        hide: function () {
          pendingShow = false;
          $timeout.cancel(fallbackTimer);
          removeClickBlock();
        }
      };
    }]);

  /**
   * @ngdoc service
   * @name $ionicGesture
   * @module ionic
   * @description An angular service exposing ionic
   * {@link ionic.utility:ionic.EventController}'s gestures.
   */
  IonicModule
  .factory('$ionicGesture', [function () {
    return {
      /**
       * @ngdoc method
       * @name $ionicGesture#on
       * @description Add an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#onGesture}.
       * @param {string} eventType The gesture event to listen for.
       * @param {function(e)} callback The function to call when the gesture
       * happens.
       * @param {element} $element The angular element to listen for the event on.
       * @param {object} options object.
       * @returns {ionic.Gesture} The gesture object (use this to remove the gesture later on).
       */
      on: function (eventType, cb, $element, options) {
        return window.ionic.onGesture(eventType, cb, $element[0], options);
      },
      /**
       * @ngdoc method
       * @name $ionicGesture#off
       * @description Remove an event listener for a gesture on an element. See {@link ionic.utility:ionic.EventController#offGesture}.
       * @param {ionic.Gesture} gesture The gesture that should be removed.
       * @param {string} eventType The gesture event to remove the listener for.
       * @param {function(e)} callback The listener to remove.
       */
      off: function (gesture, eventType, cb) {
        return window.ionic.offGesture(gesture, eventType, cb);
      }
    };
  }]);

  /**
   * @ngdoc service
   * @name $ionicHistory
   * @module ionic
   * @description
   * $ionicHistory keeps track of views as the user navigates through an app. Similar to the way a
   * browser behaves, an Ionic app is able to keep track of the previous view, the current view, and
   * the forward view (if there is one).  However, a typical web browser only keeps track of one
   * history stack in a linear fashion.
   *
   * Unlike a traditional browser environment, apps and webapps have parallel independent histories,
   * such as with tabs. Should a user navigate few pages deep on one tab, and then switch to a new
   * tab and back, the back button relates not to the previous tab, but to the previous pages
   * visited within _that_ tab.
   *
   * `$ionicHistory` facilitates this parallel history architecture.
   */

  IonicModule
  .factory('$ionicHistory', [
    '$rootScope',
    '$state',
    '$location',
    '$window',
    '$timeout',
    '$ionicViewSwitcher',
    '$ionicNavViewDelegate',
    function ($rootScope, $state, $location, $window, $timeout, $ionicViewSwitcher, $ionicNavViewDelegate) {

      // history actions while navigating views
      var ACTION_INITIAL_VIEW = 'initialView';
      var ACTION_NEW_VIEW = 'newView';
      var ACTION_MOVE_BACK = 'moveBack';
      var ACTION_MOVE_FORWARD = 'moveForward';

      // direction of navigation
      var DIRECTION_BACK = 'back';
      var DIRECTION_FORWARD = 'forward';
      var DIRECTION_ENTER = 'enter';
      var DIRECTION_EXIT = 'exit';
      var DIRECTION_SWAP = 'swap';
      var DIRECTION_NONE = 'none';

      var stateChangeCounter = 0;
      var lastStateId, nextViewOptions, deregisterStateChangeListener, nextViewExpireTimer, forcedNav;

      var viewHistory = {
        histories: {root: {historyId: 'root', parentHistoryId: null, stack: [], cursor: -1}},
        views: {},
        backView: null,
        forwardView: null,
        currentView: null
      };

      var View = function () {
      };
      View.prototype.initialize = function (data) {
        if (data) {
          for (var name in data) this[name] = data[name];
          return this;
        }
        return null;
      };
      View.prototype.go = function () {

        if (this.stateName) {
          return $state.go(this.stateName, this.stateParams);
        }

        if (this.url && this.url !== $location.url()) {

          if (viewHistory.backView === this) {
            return $window.history.go(-1);
          } else if (viewHistory.forwardView === this) {
            return $window.history.go(1);
          }

          $location.url(this.url);
        }

        return null;
      };
      View.prototype.destroy = function () {
        if (this.scope) {
          this.scope.$destroy && this.scope.$destroy();
          this.scope = null;
        }
      };


      function getViewById(viewId) {
        return (viewId ? viewHistory.views[viewId] : null);
      }

      function getBackView(view) {
        return (view ? getViewById(view.backViewId) : null);
      }

      function getForwardView(view) {
        return (view ? getViewById(view.forwardViewId) : null);
      }

      function getHistoryById(historyId) {
        return (historyId ? viewHistory.histories[historyId] : null);
      }

      function getHistory(scope) {
        var histObj = getParentHistoryObj(scope);

        if (!viewHistory.histories[histObj.historyId]) {
          // this history object exists in parent scope, but doesn't
          // exist in the history data yet
          viewHistory.histories[histObj.historyId] = {
            historyId: histObj.historyId,
            parentHistoryId: getParentHistoryObj(histObj.scope.$parent).historyId,
            stack: [],
            cursor: -1
          };
        }
        return getHistoryById(histObj.historyId);
      }

      function getParentHistoryObj(scope) {
        var parentScope = scope;
        while (parentScope) {
          if (parentScope.hasOwnProperty('$historyId')) {
            // this parent scope has a historyId
            return {historyId: parentScope.$historyId, scope: parentScope};
          }
          // nothing found keep climbing up
          parentScope = parentScope.$parent;
        }
        // no history for the parent, use the root
        return {historyId: 'root', scope: $rootScope};
      }

      function setNavViews(viewId) {
        viewHistory.currentView = getViewById(viewId);
        viewHistory.backView = getBackView(viewHistory.currentView);
        viewHistory.forwardView = getForwardView(viewHistory.currentView);
      }

      function getCurrentStateId() {
        var id;
        if ($state && $state.current && $state.current.name) {
          id = $state.current.name;
          if ($state.params) {
            for (var key in $state.params) {
              if ($state.params.hasOwnProperty(key) && $state.params[key]) {
                id += "_" + key + "=" + $state.params[key];
              }
            }
          }
          return id;
        }
        // if something goes wrong make sure its got a unique stateId
        return ionic.Utils.nextUid();
      }

      function getCurrentStateParams() {
        var rtn;
        if ($state && $state.params) {
          for (var key in $state.params) {
            if ($state.params.hasOwnProperty(key)) {
              rtn = rtn || {};
              rtn[key] = $state.params[key];
            }
          }
        }
        return rtn;
      }


      return {

        register: function (parentScope, viewLocals) {

          var currentStateId = getCurrentStateId(),
            hist = getHistory(parentScope),
            currentView = viewHistory.currentView,
            backView = viewHistory.backView,
            forwardView = viewHistory.forwardView,
            viewId = null,
            action = null,
            direction = DIRECTION_NONE,
            historyId = hist.historyId,
            url = $location.url(),
            tmp, x, ele;

          if (lastStateId !== currentStateId) {
            lastStateId = currentStateId;
            stateChangeCounter++;
          }

          if (forcedNav) {
            // we've previously set exactly what to do
            viewId = forcedNav.viewId;
            action = forcedNav.action;
            direction = forcedNav.direction;
            forcedNav = null;

          } else if (backView && backView.stateId === currentStateId) {
            // they went back one, set the old current view as a forward view
            viewId = backView.viewId;
            historyId = backView.historyId;
            action = ACTION_MOVE_BACK;
            if (backView.historyId === currentView.historyId) {
              // went back in the same history
              direction = DIRECTION_BACK;

            } else if (currentView) {
              direction = DIRECTION_EXIT;

              tmp = getHistoryById(backView.historyId);
              if (tmp && tmp.parentHistoryId === currentView.historyId) {
                direction = DIRECTION_ENTER;

              } else {
                tmp = getHistoryById(currentView.historyId);
                if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
                  direction = DIRECTION_SWAP;
                }
              }
            }

          } else if (forwardView && forwardView.stateId === currentStateId) {
            // they went to the forward one, set the forward view to no longer a forward view
            viewId = forwardView.viewId;
            historyId = forwardView.historyId;
            action = ACTION_MOVE_FORWARD;
            if (forwardView.historyId === currentView.historyId) {
              direction = DIRECTION_FORWARD;

            } else if (currentView) {
              direction = DIRECTION_EXIT;

              if (currentView.historyId === hist.parentHistoryId) {
                direction = DIRECTION_ENTER;

              } else {
                tmp = getHistoryById(currentView.historyId);
                if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
                  direction = DIRECTION_SWAP;
                }
              }
            }

            tmp = getParentHistoryObj(parentScope);
            if (forwardView.historyId && tmp.scope) {
              // if a history has already been created by the forward view then make sure it stays the same
              tmp.scope.$historyId = forwardView.historyId;
              historyId = forwardView.historyId;
            }

          } else if (currentView && currentView.historyId !== historyId &&
            hist.cursor > -1 && hist.stack.length > 0 && hist.cursor < hist.stack.length &&
            hist.stack[hist.cursor].stateId === currentStateId) {
            // they just changed to a different history and the history already has views in it
            var switchToView = hist.stack[hist.cursor];
            viewId = switchToView.viewId;
            historyId = switchToView.historyId;
            action = ACTION_MOVE_BACK;
            direction = DIRECTION_SWAP;

            tmp = getHistoryById(currentView.historyId);
            if (tmp && tmp.parentHistoryId === historyId) {
              direction = DIRECTION_EXIT;

            } else {
              tmp = getHistoryById(historyId);
              if (tmp && tmp.parentHistoryId === currentView.historyId) {
                direction = DIRECTION_ENTER;
              }
            }

            // if switching to a different history, and the history of the view we're switching
            // to has an existing back view from a different history than itself, then
            // it's back view would be better represented using the current view as its back view
            tmp = getViewById(switchToView.backViewId);
            if (tmp && switchToView.historyId !== tmp.historyId) {
              // the new view is being removed from it's old position in the history and being placed at the top,
              // so we need to update any views that reference it as a backview, otherwise there will be infinitely loops
              var viewIds = Object.keys(viewHistory.views);
              viewIds.forEach(function (viewId) {
                var view = viewHistory.views[viewId];
                if ((view.backViewId === switchToView.viewId) && (view.historyId !== switchToView.historyId)) {
                  view.backViewId = null;
                }
              });

              hist.stack[hist.cursor].backViewId = currentView.viewId;
            }

          } else {

            // create an element from the viewLocals template
            ele = $ionicViewSwitcher.createViewEle(viewLocals);
            if (this.isAbstractEle(ele, viewLocals)) {
              return {
                action: 'abstractView',
                direction: DIRECTION_NONE,
                ele: ele
              };
            }

            // set a new unique viewId
            viewId = ionic.Utils.nextUid();

            if (currentView) {
              // set the forward view if there is a current view (ie: if its not the first view)
              currentView.forwardViewId = viewId;

              action = ACTION_NEW_VIEW;

              // check if there is a new forward view within the same history
              if (forwardView && currentView.stateId !== forwardView.stateId &&
                currentView.historyId === forwardView.historyId) {
                // they navigated to a new view but the stack already has a forward view
                // since its a new view remove any forwards that existed
                tmp = getHistoryById(forwardView.historyId);
                if (tmp) {
                  // the forward has a history
                  for (x = tmp.stack.length - 1; x >= forwardView.index; x--) {
                    // starting from the end destroy all forwards in this history from this point
                    var stackItem = tmp.stack[x];
                    stackItem && stackItem.destroy && stackItem.destroy();
                    tmp.stack.splice(x);
                  }
                  historyId = forwardView.historyId;
                }
              }

              // its only moving forward if its in the same history
              if (hist.historyId === currentView.historyId) {
                direction = DIRECTION_FORWARD;

              } else if (currentView.historyId !== hist.historyId) {
                // DB: this is a new view in a different tab
                direction = DIRECTION_ENTER;

                tmp = getHistoryById(currentView.historyId);
                if (tmp && tmp.parentHistoryId === hist.parentHistoryId) {
                  direction = DIRECTION_SWAP;

                } else {
                  tmp = getHistoryById(tmp.parentHistoryId);
                  if (tmp && tmp.historyId === hist.historyId) {
                    direction = DIRECTION_EXIT;
                  }
                }
              }

            } else {
              // there's no current view, so this must be the initial view
              action = ACTION_INITIAL_VIEW;
            }

            if (stateChangeCounter < 2) {
              // views that were spun up on the first load should not animate
              direction = DIRECTION_NONE;
            }

            // add the new view
            viewHistory.views[viewId] = this.createView({
              viewId: viewId,
              index: hist.stack.length,
              historyId: hist.historyId,
              backViewId: (currentView && currentView.viewId ? currentView.viewId : null),
              forwardViewId: null,
              stateId: currentStateId,
              stateName: this.currentStateName(),
              stateParams: getCurrentStateParams(),
              url: url,
              canSwipeBack: canSwipeBack(ele, viewLocals)
            });

            // add the new view to this history's stack
            hist.stack.push(viewHistory.views[viewId]);
          }

          deregisterStateChangeListener && deregisterStateChangeListener();
          $timeout.cancel(nextViewExpireTimer);
          if (nextViewOptions) {
            if (nextViewOptions.disableAnimate) direction = DIRECTION_NONE;
            if (nextViewOptions.disableBack) viewHistory.views[viewId].backViewId = null;
            if (nextViewOptions.historyRoot) {
              for (x = 0; x < hist.stack.length; x++) {
                if (hist.stack[x].viewId === viewId) {
                  hist.stack[x].index = 0;
                  hist.stack[x].backViewId = hist.stack[x].forwardViewId = null;
                } else {
                  delete viewHistory.views[hist.stack[x].viewId];
                }
              }
              hist.stack = [viewHistory.views[viewId]];
            }
            nextViewOptions = null;
          }

          setNavViews(viewId);

          if (viewHistory.backView && historyId == viewHistory.backView.historyId && currentStateId == viewHistory.backView.stateId && url == viewHistory.backView.url) {
            for (x = 0; x < hist.stack.length; x++) {
              if (hist.stack[x].viewId == viewId) {
                action = 'dupNav';
                direction = DIRECTION_NONE;
                if (x > 0) {
                  hist.stack[x - 1].forwardViewId = null;
                }
                viewHistory.forwardView = null;
                viewHistory.currentView.index = viewHistory.backView.index;
                viewHistory.currentView.backViewId = viewHistory.backView.backViewId;
                viewHistory.backView = getBackView(viewHistory.backView);
                hist.stack.splice(x, 1);
                break;
              }
            }
          }

          hist.cursor = viewHistory.currentView.index;

          return {
            viewId: viewId,
            action: action,
            direction: direction,
            historyId: historyId,
            enableBack: this.enabledBack(viewHistory.currentView),
            isHistoryRoot: (viewHistory.currentView.index === 0),
            ele: ele
          };
        },

        registerHistory: function (scope) {
          scope.$historyId = ionic.Utils.nextUid();
        },

        createView: function (data) {
          var newView = new View();
          return newView.initialize(data);
        },

        getViewById: getViewById,

        /**
         * @ngdoc method
         * @name $ionicHistory#viewHistory
         * @description The app's view history data, such as all the views and histories, along
         * with how they are ordered and linked together within the navigation stack.
         * @returns {object} Returns an object containing the apps view history data.
         */
        viewHistory: function () {
          return viewHistory;
        },

        /**
         * @ngdoc method
         * @name $ionicHistory#currentView
         * @description The app's current view.
         * @returns {object} Returns the current view.
         */
        currentView: function (view) {
          if (arguments.length) {
            viewHistory.currentView = view;
          }
          return viewHistory.currentView;
        },

        /**
         * @ngdoc method
         * @name $ionicHistory#currentHistoryId
         * @description The ID of the history stack which is the parent container of the current view.
         * @returns {string} Returns the current history ID.
         */
        currentHistoryId: function () {
          return viewHistory.currentView ? viewHistory.currentView.historyId : null;
        },

        /**
         * @ngdoc method
         * @name $ionicHistory#currentTitle
         * @description Gets and sets the current view's title.
         * @param {string=} val The title to update the current view with.
         * @returns {string} Returns the current view's title.
         */
        currentTitle: function (val) {
          if (viewHistory.currentView) {
            if (arguments.length) {
              viewHistory.currentView.title = val;
            }
            return viewHistory.currentView.title;
          }
        },

        /**
         * @ngdoc method
         * @name $ionicHistory#backView
         * @description Returns the view that was before the current view in the history stack.
         * If the user navigated from View A to View B, then View A would be the back view, and
         * View B would be the current view.
         * @returns {object} Returns the back view.
         */
        backView: function (view) {
          if (arguments.length) {
            viewHistory.backView = view;
          }
          return viewHistory.backView;
        },

        /**
         * @ngdoc method
         * @name $ionicHistory#backTitle
         * @description Gets the back view's title.
         * @returns {string} Returns the back view's title.
         */
        backTitle: function (view) {
          var backView = (view && getViewById(view.backViewId)) || viewHistory.backView;
          return backView && backView.title;
        },

        /**
         * @ngdoc method
         * @name $ionicHistory#forwardView
         * @description Returns the view that was in front of the current view in the history stack.
         * A forward view would exist if the user navigated from View A to View B, then
         * navigated back to View A. At this point then View B would be the forward view, and View
         * A would be the current view.
         * @returns {object} Returns the forward view.
         */
        forwardView: function (view) {
          if (arguments.length) {
            viewHistory.forwardView = view;
          }
          return viewHistory.forwardView;
        },

        /**
         * @ngdoc method
         * @name $ionicHistory#currentStateName
         * @description Returns the current state name.
         * @returns {string}
         */
        currentStateName: function () {
          return ($state && $state.current ? $state.current.name : null);
        },

        isCurrentStateNavView: function (navView) {
          return !!($state && $state.current && $state.current.views && $state.current.views[navView]);
        },

        goToHistoryRoot: function (historyId) {
          if (historyId) {
            var hist = getHistoryById(historyId);
            if (hist && hist.stack.length) {
              if (viewHistory.currentView && viewHistory.currentView.viewId === hist.stack[0].viewId) {
                return;
              }
              forcedNav = {
                viewId: hist.stack[0].viewId,
                action: ACTION_MOVE_BACK,
                direction: DIRECTION_BACK
              };
              hist.stack[0].go();
            }
          }
        },

        /**
         * @ngdoc method
         * @name $ionicHistory#goBack
         * @param {number=} backCount Optional negative integer setting how many views to go
         * back. By default it'll go back one view by using the value `-1`. To go back two
         * views you would use `-2`. If the number goes farther back than the number of views
         * in the current history's stack then it'll go to the first view in the current history's
         * stack. If the number is zero or greater then it'll do nothing. It also does not
         * cross history stacks, meaning it can only go as far back as the current history.
         * @description Navigates the app to the back view, if a back view exists.
         */
        goBack: function (backCount) {
          if (isDefined(backCount) && backCount !== -1) {
            if (backCount > -1) return;

            var currentHistory = viewHistory.histories[this.currentHistoryId()];
            var newCursor = currentHistory.cursor + backCount + 1;
            if (newCursor < 1) {
              newCursor = 1;
            }

            currentHistory.cursor = newCursor;
            setNavViews(currentHistory.stack[newCursor].viewId);

            var cursor = newCursor - 1;
            var clearStateIds = [];
            var fwdView = getViewById(currentHistory.stack[cursor].forwardViewId);
            while (fwdView) {
              clearStateIds.push(fwdView.stateId || fwdView.viewId);
              cursor++;
              if (cursor >= currentHistory.stack.length) break;
              fwdView = getViewById(currentHistory.stack[cursor].forwardViewId);
            }

            var self = this;
            if (clearStateIds.length) {
              $timeout(function () {
                self.clearCache(clearStateIds);
              }, 300);
            }
          }

          viewHistory.backView && viewHistory.backView.go();
        },

        /**
         * @ngdoc method
         * @name $ionicHistory#removeBackView
         * @description Remove the previous view from the history completely, including the
         * cached element and scope (if they exist).
         */
        removeBackView: function () {
          var self = this;
          var currentHistory = viewHistory.histories[this.currentHistoryId()];
          var currentCursor = currentHistory.cursor;

          var currentView = currentHistory.stack[currentCursor];
          var backView = currentHistory.stack[currentCursor - 1];
          var replacementView = currentHistory.stack[currentCursor - 2];

          // fail if we dont have enough views in the history
          if (!backView || !replacementView) {
            return;
          }

          // remove the old backView and the cached element/scope
          currentHistory.stack.splice(currentCursor - 1, 1);
          self.clearCache([backView.viewId]);
          // make the replacementView and currentView point to each other (bypass the old backView)
          currentView.backViewId = replacementView.viewId;
          currentView.index = currentView.index - 1;
          replacementView.forwardViewId = currentView.viewId;
          // update the cursor and set new backView
          viewHistory.backView = replacementView;
          currentHistory.currentCursor += -1;
        },

        enabledBack: function (view) {
          var backView = getBackView(view);
          return !!(backView && backView.historyId === view.historyId);
        },

        /**
         * @ngdoc method
         * @name $ionicHistory#clearHistory
         * @description Clears out the app's entire history, except for the current view.
         */
        clearHistory: function () {
          var
            histories = viewHistory.histories,
            currentView = viewHistory.currentView;

          if (histories) {
            for (var historyId in histories) {

              if (histories[historyId].stack) {
                histories[historyId].stack = [];
                histories[historyId].cursor = -1;
              }

              if (currentView && currentView.historyId === historyId) {
                currentView.backViewId = currentView.forwardViewId = null;
                histories[historyId].stack.push(currentView);
              } else if (histories[historyId].destroy) {
                histories[historyId].destroy();
              }

            }
          }

          for (var viewId in viewHistory.views) {
            if (viewId !== currentView.viewId) {
              delete viewHistory.views[viewId];
            }
          }

          if (currentView) {
            setNavViews(currentView.viewId);
          }
        },

        /**
         * @ngdoc method
         * @name $ionicHistory#clearCache
         * @return promise
         * @description Removes all cached views within every {@link ionic.directive:ionNavView}.
         * This both removes the view element from the DOM, and destroy it's scope.
         */
        clearCache: function (stateIds) {
          return $timeout(function () {
            $ionicNavViewDelegate._instances.forEach(function (instance) {
              instance.clearCache(stateIds);
            });
          });
        },

        /**
         * @ngdoc method
         * @name $ionicHistory#nextViewOptions
         * @description Sets options for the next view. This method can be useful to override
         * certain view/transition defaults right before a view transition happens. For example,
         * the {@link ionic.directive:menuClose} directive uses this method internally to ensure
         * an animated view transition does not happen when a side menu is open, and also sets
         * the next view as the root of its history stack. After the transition these options
         * are set back to null.
         *
         * Available options:
         *
         * * `disableAnimate`: Do not animate the next transition.
         * * `disableBack`: The next view should forget its back view, and set it to null.
         * * `historyRoot`: The next view should become the root view in its history stack.
         *
         * ```js
         * $ionicHistory.nextViewOptions({
         *   disableAnimate: true,
         *   disableBack: true
         * });
         * ```
         */
        nextViewOptions: function (opts) {
          deregisterStateChangeListener && deregisterStateChangeListener();
          if (arguments.length) {
            $timeout.cancel(nextViewExpireTimer);
            if (opts === null) {
              nextViewOptions = opts;
            } else {
              nextViewOptions = nextViewOptions || {};
              extend(nextViewOptions, opts);
              if (nextViewOptions.expire) {
                deregisterStateChangeListener = $rootScope.$on('$stateChangeSuccess', function () {
                  nextViewExpireTimer = $timeout(function () {
                    nextViewOptions = null;
                  }, nextViewOptions.expire);
                });
              }
            }
          }
          return nextViewOptions;
        },

        isAbstractEle: function (ele, viewLocals) {
          if (viewLocals && viewLocals.$$state && viewLocals.$$state.self['abstract']) {
            return true;
          }
          return !!(ele && (isAbstractTag(ele) || isAbstractTag(ele.children())));
        },

        isActiveScope: function (scope) {
          if (!scope) return false;

          var climbScope = scope;
          var currentHistoryId = this.currentHistoryId();
          var foundHistoryId;

          while (climbScope) {
            if (climbScope.$$disconnected) {
              return false;
            }

            if (!foundHistoryId && climbScope.hasOwnProperty('$historyId')) {
              foundHistoryId = true;
            }

            if (currentHistoryId) {
              if (climbScope.hasOwnProperty('$historyId') && currentHistoryId == climbScope.$historyId) {
                return true;
              }
              if (climbScope.hasOwnProperty('$activeHistoryId')) {
                if (currentHistoryId == climbScope.$activeHistoryId) {
                  if (climbScope.hasOwnProperty('$historyId')) {
                    return true;
                  }
                  if (!foundHistoryId) {
                    return true;
                  }
                }
              }
            }

            if (foundHistoryId && climbScope.hasOwnProperty('$activeHistoryId')) {
              foundHistoryId = false;
            }

            climbScope = climbScope.$parent;
          }

          return currentHistoryId ? currentHistoryId == 'root' : true;
        }

      };

      function isAbstractTag(ele) {
        return ele && ele.length && /ion-side-menus|ion-tabs/i.test(ele[0].tagName);
      }

      function canSwipeBack(ele, viewLocals) {
        if (viewLocals && viewLocals.$$state && viewLocals.$$state.self.canSwipeBack === false) {
          return false;
        }
        if (ele && ele.attr('can-swipe-back') === 'false') {
          return false;
        }
        var eleChild = ele.find('ion-view');
        if (eleChild && eleChild.attr('can-swipe-back') === 'false') {
          return false;
        }
        return true;
      }

    }])

  .run([
    '$rootScope',
    '$state',
    '$location',
    '$document',
    '$ionicPlatform',
    '$ionicHistory',
    'IONIC_BACK_PRIORITY',
    function ($rootScope, $state, $location, $document, $ionicPlatform, $ionicHistory, IONIC_BACK_PRIORITY) {

      // always reset the keyboard state when change stage
      $rootScope.$on('$ionicView.beforeEnter', function () {
        ionic.keyboard && ionic.keyboard.hide && ionic.keyboard.hide();
      });

      $rootScope.$on('$ionicHistory.change', function (e, data) {
        if (!data) return null;

        var viewHistory = $ionicHistory.viewHistory();

        var hist = (data.historyId ? viewHistory.histories[data.historyId] : null);
        if (hist && hist.cursor > -1 && hist.cursor < hist.stack.length) {
          // the history they're going to already exists
          // go to it's last view in its stack
          var view = hist.stack[hist.cursor];
          return view.go(data);
        }

        // this history does not have a URL, but it does have a uiSref
        // figure out its URL from the uiSref
        if (!data.url && data.uiSref) {
          data.url = $state.href(data.uiSref);
        }

        if (data.url) {
          // don't let it start with a #, messes with $location.url()
          if (data.url.indexOf('#') === 0) {
            data.url = data.url.replace('#', '');
          }
          if (data.url !== $location.url()) {
            // we've got a good URL, ready GO!
            $location.url(data.url);
          }
        }
      });

      $rootScope.$ionicGoBack = function (backCount) {
        $ionicHistory.goBack(backCount);
      };

      // Set the document title when a new view is shown
      $rootScope.$on('$ionicView.afterEnter', function (ev, data) {
        if (data && data.title) {
          $document[0].title = data.title;
        }
      });

      // Triggered when devices with a hardware back button (Android) is clicked by the user
      // This is a Cordova/Phonegap platform specifc method
      function onHardwareBackButton(e) {
        var backView = $ionicHistory.backView();
        if (backView) {
          // there is a back view, go to it
          backView.go();
        } else {
          // there is no back view, so close the app instead
          ionic.Platform.exitApp();
        }
        e.preventDefault();
        return false;
      }

      $ionicPlatform.registerBackButtonAction(
        onHardwareBackButton,
        IONIC_BACK_PRIORITY.view
      );

    }]);

  /**
   * @ngdoc provider
   * @name $ionicConfigProvider
   * @module ionic
   * @description
   * Ionic automatically takes platform configurations into account to adjust things like what
   * transition style to use and whether tab icons should show on the top or bottom. For example,
   * iOS will move forward by transitioning the entering view from right to center and the leaving
   * view from center to left. However, Android will transition with the entering view going from
   * bottom to center, covering the previous view, which remains stationary. It should be noted
   * that when a platform is not iOS or Android, then it'll default to iOS. So if you are
   * developing on a desktop browser, it's going to take on iOS default configs.
   *
   * These configs can be changed using the `$ionicConfigProvider` during the configuration phase
   * of your app. Additionally, `$ionicConfig` can also set and get config values during the run
   * phase and within the app itself.
   *
   * By default, all base config variables are set to `'platform'`, which means it'll take on the
   * default config of the platform on which it's running. Config variables can be set at this
   * level so all platforms follow the same setting, rather than its platform config.
   * The following code would set the same config variable for all platforms:
   *
   * ```js
   * $ionicConfigProvider.views.maxCache(10);
   * ```
   *
   * Additionally, each platform can have its own config within the `$ionicConfigProvider.platform`
   * property. The config below would only apply to Android devices.
   *
   * ```js
   * $ionicConfigProvider.platform.android.views.maxCache(5);
   * ```
   *
   * @usage
   * ```js
   * var learn-ionic-main = angular.module('reallyCoolApp', ['ionic']);
   *
   * learn-ionic-main.config(function($ionicConfigProvider) {
   *   $ionicConfigProvider.views.maxCache(5);
   *
   *   // note that you can also chain configs
   *   $ionicConfigProvider.backButton.text('Go Back').icon('ion-chevron-left');
   * });
   * ```
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#views.transition
   * @description Animation style when transitioning between views. Default `platform`.
   *
   * @param {string} transition Which style of view transitioning to use.
   *
   * * `platform`: Dynamically choose the correct transition style depending on the platform
   * the app is running from. If the platform is not `ios` or `android` then it will default
   * to `ios`.
   * * `ios`: iOS style transition.
   * * `android`: Android style transition.
   * * `none`: Do not perform animated transitions.
   *
   * @returns {string} value
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#views.maxCache
   * @description  Maximum number of view elements to cache in the DOM. When the max number is
   * exceeded, the view with the longest time period since it was accessed is removed. Views that
   * stay in the DOM cache the view's scope, current state, and scroll position. The scope is
   * disconnected from the `$watch` cycle when it is cached and reconnected when it enters again.
   * When the maximum cache is `0`, the leaving view's element will be removed from the DOM after
   * each view transition, and the next time the same view is shown, it will have to re-compile,
   * attach to the DOM, and link the element again. This disables caching, in effect.
   * @param {number} maxNumber Maximum number of views to retain. Default `10`.
   * @returns {number} How many views Ionic will hold onto until the a view is removed.
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#views.forwardCache
   * @description  By default, when navigating, views that were recently visited are cached, and
   * the same instance data and DOM elements are referenced when navigating back. However, when
   * navigating back in the history, the "forward" views are removed from the cache. If you
   * navigate forward to the same view again, it'll create a new DOM element and controller
   * instance. Basically, any forward views are reset each time. Set this config to `true` to have
   * forward views cached and not reset on each load.
   * @param {boolean} value
   * @returns {boolean}
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#views.swipeBackEnabled
   * @description  By default on iOS devices, swipe to go back functionality is enabled by default.
   * This method can be used to disable it globally, or on a per-view basis.
   * Note: This functionality is only supported on iOS.
   * @param {boolean} value
   * @returns {boolean}
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#scrolling.jsScrolling
   * @description  Whether to use JS or Native scrolling. Defaults to native scrolling. Setting this to
   * `true` has the same effect as setting each `ion-content` to have `overflow-scroll='false'`.
   * @param {boolean} value Defaults to `false` as of Ionic 1.2
   * @returns {boolean}
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#backButton.icon
   * @description Back button icon.
   * @param {string} value
   * @returns {string}
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#backButton.text
   * @description Back button text.
   * @param {string} value Defaults to `Back`.
   * @returns {string}
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#backButton.previousTitleText
   * @description If the previous title text should become the back button text. This
   * is the default for iOS.
   * @param {boolean} value
   * @returns {boolean}
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#form.checkbox
   * @description Checkbox style. Android defaults to `square` and iOS defaults to `circle`.
   * @param {string} value
   * @returns {string}
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#form.toggle
   * @description Toggle item style. Android defaults to `small` and iOS defaults to `large`.
   * @param {string} value
   * @returns {string}
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#spinner.icon
   * @description Default spinner icon to use.
   * @param {string} value Can be: `android`, `ios`, `ios-small`, `bubbles`, `circles`, `crescent`,
   * `dots`, `lines`, `ripple`, or `spiral`.
   * @returns {string}
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#tabs.style
   * @description Tab style. Android defaults to `striped` and iOS defaults to `standard`.
   * @param {string} value Available values include `striped` and `standard`.
   * @returns {string}
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#tabs.position
   * @description Tab position. Android defaults to `top` and iOS defaults to `bottom`.
   * @param {string} value Available values include `top` and `bottom`.
   * @returns {string}
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#templates.maxPrefetch
   * @description Sets the maximum number of templates to prefetch from the templateUrls defined in
   * $stateProvider.state. If set to `0`, the user will have to wait
   * for a template to be fetched the first time when navigating to a new page. Default `30`.
   * @param {integer} value Max number of template to prefetch from the templateUrls defined in
   * `$stateProvider.state()`.
   * @returns {integer}
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#navBar.alignTitle
   * @description Which side of the navBar to align the title. Default `center`.
   *
   * @param {string} value side of the navBar to align the title.
   *
   * * `platform`: Dynamically choose the correct title style depending on the platform
   * the app is running from. If the platform is `ios`, it will default to `center`.
   * If the platform is `android`, it will default to `left`. If the platform is not
   * `ios` or `android`, it will default to `center`.
   *
   * * `left`: Left align the title in the navBar
   * * `center`: Center align the title in the navBar
   * * `right`: Right align the title in the navBar.
   *
   * @returns {string} value
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#navBar.positionPrimaryButtons
   * @description Which side of the navBar to align the primary navBar buttons. Default `left`.
   *
   * @param {string} value side of the navBar to align the primary navBar buttons.
   *
   * * `platform`: Dynamically choose the correct title style depending on the platform
   * the app is running from. If the platform is `ios`, it will default to `left`.
   * If the platform is `android`, it will default to `right`. If the platform is not
   * `ios` or `android`, it will default to `left`.
   *
   * * `left`: Left align the primary navBar buttons in the navBar
   * * `right`: Right align the primary navBar buttons in the navBar.
   *
   * @returns {string} value
   */

  /**
   * @ngdoc method
   * @name $ionicConfigProvider#navBar.positionSecondaryButtons
   * @description Which side of the navBar to align the secondary navBar buttons. Default `right`.
   *
   * @param {string} value side of the navBar to align the secondary navBar buttons.
   *
   * * `platform`: Dynamically choose the correct title style depending on the platform
   * the app is running from. If the platform is `ios`, it will default to `right`.
   * If the platform is `android`, it will default to `right`. If the platform is not
   * `ios` or `android`, it will default to `right`.
   *
   * * `left`: Left align the secondary navBar buttons in the navBar
   * * `right`: Right align the secondary navBar buttons in the navBar.
   *
   * @returns {string} value
   */

  IonicModule
  .provider('$ionicConfig', function () {

    var provider = this;
    provider.platform = {};
    var PLATFORM = 'platform';

    var configProperties = {
      views: {
        maxCache: PLATFORM,
        forwardCache: PLATFORM,
        transition: PLATFORM,
        swipeBackEnabled: PLATFORM,
        swipeBackHitWidth: PLATFORM
      },
      navBar: {
        alignTitle: PLATFORM,
        positionPrimaryButtons: PLATFORM,
        positionSecondaryButtons: PLATFORM,
        transition: PLATFORM
      },
      backButton: {
        icon: PLATFORM,
        text: PLATFORM,
        previousTitleText: PLATFORM
      },
      form: {
        checkbox: PLATFORM,
        toggle: PLATFORM
      },
      scrolling: {
        jsScrolling: PLATFORM
      },
      spinner: {
        icon: PLATFORM
      },
      tabs: {
        style: PLATFORM,
        position: PLATFORM
      },
      templates: {
        maxPrefetch: PLATFORM
      },
      platform: {}
    };
    createConfig(configProperties, provider, '');


    // Default
    // -------------------------
    setPlatformConfig('default', {

      views: {
        maxCache: 10,
        forwardCache: false,
        transition: 'ios',
        swipeBackEnabled: true,
        swipeBackHitWidth: 45
      },

      navBar: {
        alignTitle: 'center',
        positionPrimaryButtons: 'left',
        positionSecondaryButtons: 'right',
        transition: 'view'
      },

      backButton: {
        icon: 'ion-ios-arrow-back',
        text: 'Back',
        previousTitleText: true
      },

      form: {
        checkbox: 'circle',
        toggle: 'large'
      },

      scrolling: {
        jsScrolling: true
      },

      spinner: {
        icon: 'ios'
      },

      tabs: {
        style: 'standard',
        position: 'bottom'
      },

      templates: {
        maxPrefetch: 30
      }

    });


    // iOS (it is the default already)
    // -------------------------
    setPlatformConfig('ios', {});


    // Android
    // -------------------------
    setPlatformConfig('android', {

      views: {
        transition: 'android',
        swipeBackEnabled: false
      },

      navBar: {
        alignTitle: 'left',
        positionPrimaryButtons: 'right',
        positionSecondaryButtons: 'right'
      },

      backButton: {
        icon: 'ion-android-arrow-back',
        text: false,
        previousTitleText: false
      },

      form: {
        checkbox: 'square',
        toggle: 'small'
      },

      spinner: {
        icon: 'android'
      },

      tabs: {
        style: 'striped',
        position: 'top'
      },

      scrolling: {
        jsScrolling: false
      }
    });

    // Windows Phone
    // -------------------------
    setPlatformConfig('windowsphone', {
      //scrolling: {
      //  jsScrolling: false
      //}
      spinner: {
        icon: 'android'
      }
    });


    provider.transitions = {
      views: {},
      navBar: {}
    };


    // iOS Transitions
    // -----------------------
    provider.transitions.views.ios = function (enteringEle, leavingEle, direction, shouldAnimate) {

      function setStyles(ele, opacity, x, boxShadowOpacity) {
        var css = {};
        css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0;
        css.opacity = opacity;
        if (boxShadowOpacity > -1) {
          css.boxShadow = '0 0 10px rgba(0,0,0,' + (d.shouldAnimate ? boxShadowOpacity * 0.45 : 0.3) + ')';
        }
        css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)';
        ionic.DomUtil.cachedStyles(ele, css);
      }

      var d = {
        run: function (step) {
          if (direction == 'forward') {
            setStyles(enteringEle, 1, (1 - step) * 99, 1 - step); // starting at 98% prevents a flicker
            setStyles(leavingEle, (1 - 0.1 * step), step * -33, -1);

          } else if (direction == 'back') {
            setStyles(enteringEle, (1 - 0.1 * (1 - step)), (1 - step) * -33, -1);
            setStyles(leavingEle, 1, step * 100, 1 - step);

          } else {
            // swap, enter, exit
            setStyles(enteringEle, 1, 0, -1);
            setStyles(leavingEle, 0, 0, -1);
          }
        },
        shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back')
      };

      return d;
    };

    provider.transitions.navBar.ios = function (enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) {

      function setStyles(ctrl, opacity, titleX, backTextX) {
        var css = {};
        css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : '0ms';
        css.opacity = opacity === 1 ? '' : opacity;

        ctrl.setCss('buttons-left', css);
        ctrl.setCss('buttons-right', css);
        ctrl.setCss('back-button', css);

        css[ionic.CSS.TRANSFORM] = 'translate3d(' + backTextX + 'px,0,0)';
        ctrl.setCss('back-text', css);

        css[ionic.CSS.TRANSFORM] = 'translate3d(' + titleX + 'px,0,0)';
        ctrl.setCss('title', css);
      }

      function enter(ctrlA, ctrlB, step) {
        if (!ctrlA || !ctrlB) return;
        var titleX = (ctrlA.titleTextX() + ctrlA.titleWidth()) * (1 - step);
        var backTextX = (ctrlB && (ctrlB.titleTextX() - ctrlA.backButtonTextLeft()) * (1 - step)) || 0;
        setStyles(ctrlA, step, titleX, backTextX);
      }

      function leave(ctrlA, ctrlB, step) {
        if (!ctrlA || !ctrlB) return;
        var titleX = (-(ctrlA.titleTextX() - ctrlB.backButtonTextLeft()) - (ctrlA.titleLeftRight())) * step;
        setStyles(ctrlA, 1 - step, titleX, 0);
      }

      var d = {
        run: function (step) {
          var enteringHeaderCtrl = enteringHeaderBar.controller();
          var leavingHeaderCtrl = leavingHeaderBar && leavingHeaderBar.controller();
          if (d.direction == 'back') {
            leave(enteringHeaderCtrl, leavingHeaderCtrl, 1 - step);
            enter(leavingHeaderCtrl, enteringHeaderCtrl, 1 - step);
          } else {
            enter(enteringHeaderCtrl, leavingHeaderCtrl, step);
            leave(leavingHeaderCtrl, enteringHeaderCtrl, step);
          }
        },
        direction: direction,
        shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back')
      };

      return d;
    };


    // Android Transitions
    // -----------------------

    provider.transitions.views.android = function (enteringEle, leavingEle, direction, shouldAnimate) {
      shouldAnimate = shouldAnimate && (direction == 'forward' || direction == 'back');

      function setStyles(ele, x, opacity) {
        var css = {};
        css[ionic.CSS.TRANSITION_DURATION] = d.shouldAnimate ? '' : 0;
        css[ionic.CSS.TRANSFORM] = 'translate3d(' + x + '%,0,0)';
        css.opacity = opacity;
        ionic.DomUtil.cachedStyles(ele, css);
      }

      var d = {
        run: function (step) {
          if (direction == 'forward') {
            setStyles(enteringEle, (1 - step) * 99, 1); // starting at 98% prevents a flicker
            setStyles(leavingEle, step * -100, 1);

          } else if (direction == 'back') {
            setStyles(enteringEle, (1 - step) * -100, 1);
            setStyles(leavingEle, step * 100, 1);

          } else {
            // swap, enter, exit
            setStyles(enteringEle, 0, 1);
            setStyles(leavingEle, 0, 0);
          }
        },
        shouldAnimate: shouldAnimate
      };

      return d;
    };

    provider.transitions.navBar.android = function (enteringHeaderBar, leavingHeaderBar, direction, shouldAnimate) {

      function setStyles(ctrl, opacity) {
        if (!ctrl) return;
        var css = {};
        css.opacity = opacity === 1 ? '' : opacity;

        ctrl.setCss('buttons-left', css);
        ctrl.setCss('buttons-right', css);
        ctrl.setCss('back-button', css);
        ctrl.setCss('back-text', css);
        ctrl.setCss('title', css);
      }

      return {
        run: function (step) {
          setStyles(enteringHeaderBar.controller(), step);
          setStyles(leavingHeaderBar && leavingHeaderBar.controller(), 1 - step);
        },
        shouldAnimate: shouldAnimate && (direction == 'forward' || direction == 'back')
      };
    };


    // No Transition
    // -----------------------

    provider.transitions.views.none = function (enteringEle, leavingEle) {
      return {
        run: function (step) {
          provider.transitions.views.android(enteringEle, leavingEle, false, false).run(step);
        },
        shouldAnimate: false
      };
    };

    provider.transitions.navBar.none = function (enteringHeaderBar, leavingHeaderBar) {
      return {
        run: function (step) {
          provider.transitions.navBar.ios(enteringHeaderBar, leavingHeaderBar, false, false).run(step);
          provider.transitions.navBar.android(enteringHeaderBar, leavingHeaderBar, false, false).run(step);
        },
        shouldAnimate: false
      };
    };


    // private: used to set platform configs
    function setPlatformConfig(platformName, platformConfigs) {
      configProperties.platform[platformName] = platformConfigs;
      provider.platform[platformName] = {};

      addConfig(configProperties, configProperties.platform[platformName]);

      createConfig(configProperties.platform[platformName], provider.platform[platformName], '');
    }


    // private: used to recursively add new platform configs
    function addConfig(configObj, platformObj) {
      for (var n in configObj) {
        if (n != PLATFORM && configObj.hasOwnProperty(n)) {
          if (angular.isObject(configObj[n])) {
            if (!isDefined(platformObj[n])) {
              platformObj[n] = {};
            }
            addConfig(configObj[n], platformObj[n]);

          } else if (!isDefined(platformObj[n])) {
            platformObj[n] = null;
          }
        }
      }
    }


    // private: create methods for each config to get/set
    function createConfig(configObj, providerObj, platformPath) {
      forEach(configObj, function (value, namespace) {

        if (angular.isObject(configObj[namespace])) {
          // recursively drill down the config object so we can create a method for each one
          providerObj[namespace] = {};
          createConfig(configObj[namespace], providerObj[namespace], platformPath + '.' + namespace);

        } else {
          // create a method for the provider/config methods that will be exposed
          providerObj[namespace] = function (newValue) {
            if (arguments.length) {
              configObj[namespace] = newValue;
              return providerObj;
            }
            if (configObj[namespace] == PLATFORM) {
              // if the config is set to 'platform', then get this config's platform value
              var platformConfig = stringObj(configProperties.platform, ionic.Platform.platform() + platformPath + '.' + namespace);
              if (platformConfig || platformConfig === false) {
                return platformConfig;
              }
              // didnt find a specific platform config, now try the default
              return stringObj(configProperties.platform, 'default' + platformPath + '.' + namespace);
            }
            return configObj[namespace];
          };
        }

      });
    }

    function stringObj(obj, str) {
      str = str.split(".");
      for (var i = 0; i < str.length; i++) {
        if (obj && isDefined(obj[str[i]])) {
          obj = obj[str[i]];
        } else {
          return null;
        }
      }
      return obj;
    }

    provider.setPlatformConfig = setPlatformConfig;


    // private: Service definition for internal Ionic use
    /**
     * @ngdoc service
     * @name $ionicConfig
     * @module ionic
     * @private
     */
    provider.$get = function () {
      return provider;
    };
  })
  // Fix for URLs in Cordova apps on Windows Phone
  // http://blogs.msdn.com/b/msdn_answers/archive/2015/02/10/
  // running-cordova-apps-on-windows-and-windows-phone-8-1-using-ionic-angularjs-and-other-frameworks.aspx
  .config(['$compileProvider', function ($compileProvider) {
    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|sms|tel|geo|ftp|mailto|file|ghttps?|ms-appx-web|ms-appx|x-wmapp0):/);
    $compileProvider.imgSrcSanitizationWhitelist(/^\s*(https?|ftp|file|content|blob|ms-appx|ms-appx-web|x-wmapp0):|data:image\//);
  }]);


  var LOADING_TPL =
    '<div class="loading-container">' +
    '<div class="loading">' +
    '</div>' +
    '</div>';

  /**
   * @ngdoc service
   * @name $ionicLoading
   * @module ionic
   * @description
   * An overlay that can be used to indicate activity while blocking user
   * interaction.
   *
   * @usage
   * ```js
   * angular.module('LoadingApp', ['ionic'])
   * .controller('LoadingCtrl', function($scope, $ionicLoading) {
   *   $scope.show = function() {
   *     $ionicLoading.show({
   *       template: 'Loading...',
   *       duration: 3000
   *     }).then(function(){
   *        console.log("The loading indicator is now displayed");
   *     });
   *   };
   *   $scope.hide = function(){
   *     $ionicLoading.hide().then(function(){
   *        console.log("The loading indicator is now hidden");
   *     });
   *   };
   * });
   * ```
   */
  /**
   * @ngdoc object
   * @name $ionicLoadingConfig
   * @module ionic
   * @description
   * Set the default options to be passed to the {@link ionic.service:$ionicLoading} service.
   *
   * @usage
   * ```js
   * var app = angular.module('learn-ionic-main', ['ionic'])
   * app.constant('$ionicLoadingConfig', {
   *   template: 'Default Loading Template...'
   * });
   * app.controller('AppCtrl', function($scope, $ionicLoading) {
   *   $scope.showLoading = function() {
   *     //options default to values in $ionicLoadingConfig
   *     $ionicLoading.show().then(function(){
   *        console.log("The loading indicator is now displayed");
   *     });
   *   };
   * });
   * ```
   */
  IonicModule
  .constant('$ionicLoadingConfig', {
    template: '<ion-spinner></ion-spinner>'
  })
  .factory('$ionicLoading', [
    '$ionicLoadingConfig',
    '$ionicBody',
    '$ionicTemplateLoader',
    '$ionicBackdrop',
    '$timeout',
    '$q',
    '$log',
    '$compile',
    '$ionicPlatform',
    '$rootScope',
    'IONIC_BACK_PRIORITY',
    function ($ionicLoadingConfig, $ionicBody, $ionicTemplateLoader, $ionicBackdrop, $timeout, $q, $log, $compile, $ionicPlatform, $rootScope, IONIC_BACK_PRIORITY) {

      var loaderInstance;
      //default values
      var deregisterBackAction = noop;
      var deregisterStateListener1 = noop;
      var deregisterStateListener2 = noop;
      var loadingShowDelay = $q.when();

      return {
        /**
         * @ngdoc method
         * @name $ionicLoading#show
         * @description Shows a loading indicator. If the indicator is already shown,
         * it will set the options given and keep the indicator shown.
         * @returns {promise} A promise which is resolved when the loading indicator is presented.
         * @param {object} opts The options for the loading indicator. Available properties:
         *  - `{string=}` `template` The html content of the indicator.
         *  - `{string=}` `templateUrl` The url of an html template to load as the content of the indicator.
         *  - `{object=}` `scope` The scope to be a child of. Default: creates a child of $rootScope.
         *  - `{boolean=}` `noBackdrop` Whether to hide the backdrop. By default it will be shown.
         *  - `{boolean=}` `hideOnStateChange` Whether to hide the loading spinner when navigating
         *    to a new state. Default false.
         *  - `{number=}` `delay` How many milliseconds to delay showing the indicator. By default there is no delay.
         *  - `{number=}` `duration` How many milliseconds to wait until automatically
         *  hiding the indicator. By default, the indicator will be shown until `.hide()` is called.
         */
        show: showLoader,
        /**
         * @ngdoc method
         * @name $ionicLoading#hide
         * @description Hides the loading indicator, if shown.
         * @returns {promise} A promise which is resolved when the loading indicator is hidden.
         */
        hide: hideLoader,
        /**
         * @private for testing
         */
        _getLoader: getLoader
      };

      function getLoader() {
        if (!loaderInstance) {
          loaderInstance = $ionicTemplateLoader.compile({
            template: LOADING_TPL,
            appendTo: $ionicBody.get()
          })
          .then(function (self) {
            self.show = function (options) {
              var templatePromise = options.templateUrl ?
                $ionicTemplateLoader.load(options.templateUrl) :
                //options.content: deprecated
                $q.when(options.template || options.content || '');

              self.scope = options.scope || self.scope;

              if (!self.isShown) {
                //options.showBackdrop: deprecated
                self.hasBackdrop = !options.noBackdrop && options.showBackdrop !== false;
                if (self.hasBackdrop) {
                  $ionicBackdrop.retain();
                  $ionicBackdrop.getElement().addClass('backdrop-loading');
                }
              }

              if (options.duration) {
                $timeout.cancel(self.durationTimeout);
                self.durationTimeout = $timeout(
                  angular.bind(self, self.hide),
                  +options.duration
                );
              }

              deregisterBackAction();
              //Disable hardware back button while loading
              deregisterBackAction = $ionicPlatform.registerBackButtonAction(
                noop,
                IONIC_BACK_PRIORITY.loading
              );

              templatePromise.then(function (html) {
                if (html) {
                  var loading = self.element.children();
                  loading.html(html);
                  $compile(loading.contents())(self.scope);
                }

                //Don't show until template changes
                if (self.isShown) {
                  self.element.addClass('visible');
                  ionic.requestAnimationFrame(function () {
                    if (self.isShown) {
                      self.element.addClass('active');
                      $ionicBody.addClass('loading-active');
                    }
                  });
                }
              });

              self.isShown = true;
            };
            self.hide = function () {

              deregisterBackAction();
              if (self.isShown) {
                if (self.hasBackdrop) {
                  $ionicBackdrop.release();
                  $ionicBackdrop.getElement().removeClass('backdrop-loading');
                }
                self.element.removeClass('active');
                $ionicBody.removeClass('loading-active');
                self.element.removeClass('visible');
                ionic.requestAnimationFrame(function () {
                  !self.isShown && self.element.removeClass('visible');
                });
              }
              $timeout.cancel(self.durationTimeout);
              self.isShown = false;
              var loading = self.element.children();
              loading.html("");
            };

            return self;
          });
        }
        return loaderInstance;
      }

      function showLoader(options) {
        options = extend({}, $ionicLoadingConfig || {}, options || {});
        // use a default delay of 100 to avoid some issues reported on github
        // https://github.com/ionic-team/ionic/issues/3717
        var delay = options.delay || options.showDelay || 0;

        deregisterStateListener1();
        deregisterStateListener2();
        if (options.hideOnStateChange) {
          deregisterStateListener1 = $rootScope.$on('$stateChangeSuccess', hideLoader);
          deregisterStateListener2 = $rootScope.$on('$stateChangeError', hideLoader);
        }

        //If loading.show() was called previously, cancel it and show with our new options
        $timeout.cancel(loadingShowDelay);
        loadingShowDelay = $timeout(noop, delay);
        return loadingShowDelay.then(getLoader).then(function (loader) {
          return loader.show(options);
        });
      }

      function hideLoader() {
        deregisterStateListener1();
        deregisterStateListener2();
        $timeout.cancel(loadingShowDelay);
        return getLoader().then(function (loader) {
          return loader.hide();
        });
      }
    }]);

  /**
   * @ngdoc service
   * @name $ionicModal
   * @module ionic
   * @codepen gblny
   * @description
   *
   * Related: {@link ionic.controller:ionicModal ionicModal controller}.
   *
   * The Modal is a content pane that can go over the user's main view
   * temporarily.  Usually used for making a choice or editing an item.
   *
   * Put the content of the modal inside of an `<ion-modal-view>` element.
   *
   * **Notes:**
   * - A modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating
   * scope, passing in itself as an event argument. Both the modal.removed and modal.hidden events are
   * called when the modal is removed.
   *
   * - This example assumes your modal is in your main index file or another template file. If it is in its own
   * template file, remove the script tags and call it by file name.
   *
   * @usage
   * ```html
   * <script id="my-modal.html" type="text/ng-template">
   *   <ion-modal-view>
   *     <ion-header-bar>
   *       <h1 class="title">My Modal title</h1>
   *     </ion-header-bar>
   *     <ion-content>
   *       Hello!
   *     </ion-content>
   *   </ion-modal-view>
   * </script>
   * ```
   * ```js
   * angular.module('testApp', ['ionic'])
   * .controller('MyController', function($scope, $ionicModal) {
   *   $ionicModal.fromTemplateUrl('my-modal.html', {
   *     scope: $scope,
   *     animation: 'slide-in-up'
   *   }).then(function(modal) {
   *     $scope.modal = modal;
   *   });
   *   $scope.openModal = function() {
   *     $scope.modal.show();
   *   };
   *   $scope.closeModal = function() {
   *     $scope.modal.hide();
   *   };
   *   // Cleanup the modal when we're done with it!
   *   $scope.$on('$destroy', function() {
   *     $scope.modal.remove();
   *   });
   *   // Execute action on hide modal
   *   $scope.$on('modal.hidden', function() {
   *     // Execute action
   *   });
   *   // Execute action on remove modal
   *   $scope.$on('modal.removed', function() {
   *     // Execute action
   *   });
   * });
   * ```
   */
  IonicModule
  .factory('$ionicModal', [
    '$rootScope',
    '$ionicBody',
    '$compile',
    '$timeout',
    '$ionicPlatform',
    '$ionicTemplateLoader',
    '$$q',
    '$log',
    '$ionicClickBlock',
    '$window',
    'IONIC_BACK_PRIORITY',
    function ($rootScope, $ionicBody, $compile, $timeout, $ionicPlatform, $ionicTemplateLoader, $$q, $log, $ionicClickBlock, $window, IONIC_BACK_PRIORITY) {

      /**
       * @ngdoc controller
       * @name ionicModal
       * @module ionic
       * @description
       * Instantiated by the {@link ionic.service:$ionicModal} service.
       *
       * Be sure to call [remove()](#remove) when you are done with each modal
       * to clean it up and avoid memory leaks.
       *
       * Note: a modal will broadcast 'modal.shown', 'modal.hidden', and 'modal.removed' events from its originating
       * scope, passing in itself as an event argument. Note: both modal.removed and modal.hidden are
       * called when the modal is removed.
       */
      var ModalView = ionic.views.Modal.inherit({
        /**
         * @ngdoc method
         * @name ionicModal#initialize
         * @description Creates a new modal controller instance.
         * @param {object} options An options object with the following properties:
         *  - `{object=}` `scope` The scope to be a child of.
         *    Default: creates a child of $rootScope.
         *  - `{string=}` `animation` The animation to show & hide with.
         *    Default: 'slide-in-up'
         *  - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of
         *    the modal when shown. Will only show the keyboard on iOS, to force the keyboard to show
         *    on Android, please use the [Ionic keyboard plugin](https://github.com/ionic-team/ionic-plugin-keyboard#keyboardshow).
         *    Default: false.
         *  - `{boolean=}` `backdropClickToClose` Whether to close the modal on clicking the backdrop.
         *    Default: true.
         *  - `{boolean=}` `hardwareBackButtonClose` Whether the modal can be closed using the hardware
         *    back button on Android and similar devices.  Default: true.
         */
        initialize: function (opts) {
          ionic.views.Modal.prototype.initialize.call(this, opts);
          this.animation = opts.animation || 'slide-in-up';
        },

        /**
         * @ngdoc method
         * @name ionicModal#show
         * @description Show this modal instance.
         * @returns {promise} A promise which is resolved when the modal is finished animating in.
         */
        show: function (target) {
          var self = this;

          if (self.scope.$$destroyed) {
            $log.error('Cannot call ' + self.viewType + '.show() after remove(). Please create a new ' + self.viewType + ' instance.');
            return $$q.when();
          }

          // on iOS, clicks will sometimes bleed through/ghost click on underlying
          // elements
          $ionicClickBlock.show(600);
          stack.add(self);

          var modalEl = jqLite(self.modalEl);

          self.el.classList.remove('hide');
          $timeout(function () {
            if (!self._isShown) return;
            $ionicBody.addClass(self.viewType + '-open');
          }, 400, false);

          if (!self.el.parentElement) {
            modalEl.addClass(self.animation);
            $ionicBody.append(self.el);
          }

          // if modal was closed while the keyboard was up, reset scroll view on
          // next show since we can only resize it once it's visible
          var scrollCtrl = modalEl.data('$$ionicScrollController');
          scrollCtrl && scrollCtrl.resize();

          if (target && self.positionView) {
            self.positionView(target, modalEl);
            // set up a listener for in case the window size changes

            self._onWindowResize = function () {
              if (self._isShown) self.positionView(target, modalEl);
            };
            ionic.on('resize', self._onWindowResize, window);
          }

          modalEl.addClass('ng-enter active')
          .removeClass('ng-leave ng-leave-active');

          self._isShown = true;
          self._deregisterBackButton = $ionicPlatform.registerBackButtonAction(
            self.hardwareBackButtonClose ? angular.bind(self, self.hide) : noop,
            IONIC_BACK_PRIORITY.modal
          );

          ionic.views.Modal.prototype.show.call(self);

          $timeout(function () {
            if (!self._isShown) return;
            modalEl.addClass('ng-enter-active');
            ionic.trigger('resize');
            self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.shown', self);
            self.el.classList.add('active');
            self.scope.$broadcast('$ionicHeader.align');
            self.scope.$broadcast('$ionicFooter.align');
            self.scope.$broadcast('$ionic.modalPresented');
          }, 20);

          return $timeout(function () {
            if (!self._isShown) return;
            self.$el.on('touchmove', function (e) {
              //Don't allow scrolling while open by dragging on backdrop
              var isInScroll = ionic.DomUtil.getParentOrSelfWithClass(e.target, 'scroll');
              if (!isInScroll) {
                e.preventDefault();
              }
            });
            //After animating in, allow hide on backdrop click
            self.$el.on('click', function (e) {
              if (self.backdropClickToClose && e.target === self.el && stack.isHighest(self)) {
                self.hide();
              }
            });
          }, 400);
        },

        /**
         * @ngdoc method
         * @name ionicModal#hide
         * @description Hide this modal instance.
         * @returns {promise} A promise which is resolved when the modal is finished animating out.
         */
        hide: function () {
          var self = this;
          var modalEl = jqLite(self.modalEl);

          // on iOS, clicks will sometimes bleed through/ghost click on underlying
          // elements
          $ionicClickBlock.show(600);
          stack.remove(self);

          self.el.classList.remove('active');
          modalEl.addClass('ng-leave');

          $timeout(function () {
            if (self._isShown) return;
            modalEl.addClass('ng-leave-active')
            .removeClass('ng-enter ng-enter-active active');

            self.scope.$broadcast('$ionic.modalRemoved');
          }, 20, false);

          self.$el.off('click');
          self._isShown = false;
          self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.hidden', self);
          self._deregisterBackButton && self._deregisterBackButton();

          ionic.views.Modal.prototype.hide.call(self);

          // clean up event listeners
          if (self.positionView) {
            ionic.off('resize', self._onWindowResize, window);
          }

          return $timeout(function () {
            if (!modalStack.length) {
              $ionicBody.removeClass(self.viewType + '-open');
            }
            self.el.classList.add('hide');
          }, self.hideDelay || 320);
        },

        /**
         * @ngdoc method
         * @name ionicModal#remove
         * @description Remove this modal instance from the DOM and clean up.
         * @returns {promise} A promise which is resolved when the modal is finished animating out.
         */
        remove: function () {
          var self = this,
            deferred, promise;
          self.scope.$parent && self.scope.$parent.$broadcast(self.viewType + '.removed', self);

          // Only hide modal, when it is actually shown!
          // The hide function shows a click-block-div for a split second, because on iOS,
          // clicks will sometimes bleed through/ghost click on underlying elements.
          // However, this will make the app unresponsive for short amount of time.
          // We don't want that, if the modal window is already hidden.
          if (self._isShown) {
            promise = self.hide();
          } else {
            deferred = $$q.defer();
            deferred.resolve();
            promise = deferred.promise;
          }

          return promise.then(function () {
            self.scope.$destroy();
            self.$el.remove();
          });
        },

        /**
         * @ngdoc method
         * @name ionicModal#isShown
         * @returns boolean Whether this modal is currently shown.
         */
        isShown: function () {
          return !!this._isShown;
        }
      });

      var createModal = function (templateString, options) {
        // Create a new scope for the modal
        var scope = options.scope && options.scope.$new() || $rootScope.$new(true);

        options.viewType = options.viewType || 'modal';

        extend(scope, {
          $hasHeader: false,
          $hasSubheader: false,
          $hasFooter: false,
          $hasSubfooter: false,
          $hasTabs: false,
          $hasTabsTop: false
        });

        // Compile the template
        var element = $compile('<ion-' + options.viewType + '>' + templateString + '</ion-' + options.viewType + '>')(scope);

        options.$el = element;
        options.el = element[0];
        options.modalEl = options.el.querySelector('.' + options.viewType);
        var modal = new ModalView(options);

        modal.scope = scope;

        // If this wasn't a defined scope, we can assign the viewType to the isolated scope
        // we created
        if (!options.scope) {
          scope[options.viewType] = modal;
        }

        return modal;
      };

      var modalStack = [];
      var stack = {
        add: function (modal) {
          modalStack.push(modal);
        },
        remove: function (modal) {
          var index = modalStack.indexOf(modal);
          if (index > -1 && index < modalStack.length) {
            modalStack.splice(index, 1);
          }
        },
        isHighest: function (modal) {
          var index = modalStack.indexOf(modal);
          return (index > -1 && index === modalStack.length - 1);
        }
      };

      return {
        /**
         * @ngdoc method
         * @name $ionicModal#fromTemplate
         * @param {string} templateString The template string to use as the modal's
         * content.
         * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method.
         * @returns {object} An instance of an {@link ionic.controller:ionicModal}
         * controller.
         */
        fromTemplate: function (templateString, options) {
          var modal = createModal(templateString, options || {});
          return modal;
        },
        /**
         * @ngdoc method
         * @name $ionicModal#fromTemplateUrl
         * @param {string} templateUrl The url to load the template from.
         * @param {object} options Options to be passed {@link ionic.controller:ionicModal#initialize ionicModal#initialize} method.
         * options object.
         * @returns {promise} A promise that will be resolved with an instance of
         * an {@link ionic.controller:ionicModal} controller.
         */
        fromTemplateUrl: function (url, options, _) {
          var cb;
          //Deprecated: allow a callback as second parameter. Now we return a promise.
          if (angular.isFunction(options)) {
            cb = options;
            options = _;
          }
          return $ionicTemplateLoader.load(url).then(function (templateString) {
            var modal = createModal(templateString, options || {});
            cb && cb(modal);
            return modal;
          });
        },

        stack: stack
      };
    }]);


  /**
   * @ngdoc service
   * @name $ionicNavBarDelegate
   * @module ionic
   * @description
   * Delegate for controlling the {@link ionic.directive:ionNavBar} directive.
   *
   * @usage
   *
   * ```html
   * <body ng-controller="MyCtrl">
   *   <ion-nav-bar>
   *     <button ng-click="setNavTitle('banana')">
   *       Set title to banana!
   *     </button>
   *   </ion-nav-bar>
   * </body>
   * ```
   * ```js
   * function MyCtrl($scope, $ionicNavBarDelegate) {
   *   $scope.setNavTitle = function(title) {
   *     $ionicNavBarDelegate.title(title);
   *   }
   * }
   * ```
   */
  IonicModule
  .service('$ionicNavBarDelegate', ionic.DelegateService([
    /**
     * @ngdoc method
     * @name $ionicNavBarDelegate#align
     * @description Aligns the title with the buttons in a given direction.
     * @param {string=} direction The direction to the align the title text towards.
     * Available: 'left', 'right', 'center'. Default: 'center'.
     */
    'align',
    /**
     * @ngdoc method
     * @name $ionicNavBarDelegate#showBackButton
     * @description
     * Set/get whether the {@link ionic.directive:ionNavBackButton} is shown
     * (if it exists and there is a previous view that can be navigated to).
     * @param {boolean=} show Whether to show the back button.
     * @returns {boolean} Whether the back button is shown.
     */
    'showBackButton',
    /**
     * @ngdoc method
     * @name $ionicNavBarDelegate#showBar
     * @description
     * Set/get whether the {@link ionic.directive:ionNavBar} is shown.
     * @param {boolean} show Whether to show the bar.
     * @returns {boolean} Whether the bar is shown.
     */
    'showBar',
    /**
     * @ngdoc method
     * @name $ionicNavBarDelegate#title
     * @description
     * Set the title for the {@link ionic.directive:ionNavBar}.
     * @param {string} title The new title to show.
     */
    'title',

    // DEPRECATED, as of v1.0.0-beta14 -------
    'changeTitle',
    'setTitle',
    'getTitle',
    'back',
    'getPreviousTitle'
    // END DEPRECATED -------
  ]));


  IonicModule
  .service('$ionicNavViewDelegate', ionic.DelegateService([
    'clearCache'
  ]));


  /**
   * @ngdoc service
   * @name $ionicPlatform
   * @module ionic
   * @description
   * An angular abstraction of {@link ionic.utility:ionic.Platform}.
   *
   * Used to detect the current platform, as well as do things like override the
   * Android back button in PhoneGap/Cordova.
   */
  IonicModule
  .constant('IONIC_BACK_PRIORITY', {
    view: 100,
    sideMenu: 150,
    modal: 200,
    actionSheet: 300,
    popup: 400,
    loading: 500
  })
  .provider('$ionicPlatform', function () {
    return {
      $get: ['$q', '$ionicScrollDelegate', function ($q, $ionicScrollDelegate) {
        var self = {

          /**
           * @ngdoc method
           * @name $ionicPlatform#onHardwareBackButton
           * @description
           * Some platforms have a hardware back button, so this is one way to
           * bind to it.
           * @param {function} callback the callback to trigger when this event occurs
           */
          onHardwareBackButton: function (cb) {
            ionic.Platform.ready(function () {
              document.addEventListener('backbutton', cb, false);
            });
          },

          /**
           * @ngdoc method
           * @name $ionicPlatform#offHardwareBackButton
           * @description
           * Remove an event listener for the backbutton.
           * @param {function} callback The listener function that was
           * originally bound.
           */
          offHardwareBackButton: function (fn) {
            ionic.Platform.ready(function () {
              document.removeEventListener('backbutton', fn);
            });
          },

          /**
           * @ngdoc method
           * @name $ionicPlatform#registerBackButtonAction
           * @description
           * Register a hardware back button action. Only one action will execute
           * when the back button is clicked, so this method decides which of
           * the registered back button actions has the highest priority.
           *
           * For example, if an actionsheet is showing, the back button should
           * close the actionsheet, but it should not also go back a page view
           * or close a modal which may be open.
           *
           * The priorities for the existing back button hooks are as follows:
           *   Return to previous view = 100
           *   Close side menu = 150
           *   Dismiss modal = 200
           *   Close action sheet = 300
           *   Dismiss popup = 400
           *   Dismiss loading overlay = 500
           *
           * Your back button action will override each of the above actions
           * whose priority is less than the priority you provide. For example,
           * an action assigned a priority of 101 will override the 'return to
           * previous view' action, but not any of the other actions.
           *
           * @param {function} callback Called when the back button is pressed,
           * if this listener is the highest priority.
           * @param {number} priority Only the highest priority will execute.
           * @param {*=} actionId The id to assign this action. Default: a
           * random unique id.
           * @returns {function} A function that, when called, will deregister
           * this backButtonAction.
           */
          $backButtonActions: {},
          registerBackButtonAction: function (fn, priority, actionId) {

            if (!self._hasBackButtonHandler) {
              // add a back button listener if one hasn't been setup yet
              self.$backButtonActions = {};
              self.onHardwareBackButton(self.hardwareBackButtonClick);
              self._hasBackButtonHandler = true;
            }

            var action = {
              id: (actionId ? actionId : ionic.Utils.nextUid()),
              priority: (priority ? priority : 0),
              fn: fn
            };
            self.$backButtonActions[action.id] = action;

            // return a function to de-register this back button action
            return function () {
              delete self.$backButtonActions[action.id];
            };
          },

          /**
           * @private
           */
          hardwareBackButtonClick: function (e) {
            // loop through all the registered back button actions
            // and only run the last one of the highest priority
            var priorityAction, actionId;
            for (actionId in self.$backButtonActions) {
              if (!priorityAction || self.$backButtonActions[actionId].priority >= priorityAction.priority) {
                priorityAction = self.$backButtonActions[actionId];
              }
            }
            if (priorityAction) {
              priorityAction.fn(e);
              return priorityAction;
            }
          },

          is: function (type) {
            return ionic.Platform.is(type);
          },

          /**
           * @ngdoc method
           * @name $ionicPlatform#on
           * @description
           * Add Cordova event listeners, such as `pause`, `resume`, `volumedownbutton`, `batterylow`,
           * `offline`, etc. More information about available event types can be found in
           * [Cordova's event documentation](https://cordova.apache.org/docs/en/latest/cordova/events/events.html).
           * @param {string} type Cordova [event type](https://cordova.apache.org/docs/en/latest/cordova/events/events.html).
           * @param {function} callback Called when the Cordova event is fired.
           * @returns {function} Returns a deregistration function to remove the event listener.
           */
          on: function (type, cb) {
            ionic.Platform.ready(function () {
              document.addEventListener(type, cb, false);
            });
            return function () {
              ionic.Platform.ready(function () {
                document.removeEventListener(type, cb);
              });
            };
          },

          /**
           * @ngdoc method
           * @name $ionicPlatform#ready
           * @description
           * Trigger a callback once the device is ready,
           * or immediately if the device is already ready.
           * @param {function=} callback The function to call.
           * @returns {promise} A promise which is resolved when the device is ready.
           */
          ready: function (cb) {
            var q = $q.defer();

            ionic.Platform.ready(function () {

              window.addEventListener('statusTap', function () {
                $ionicScrollDelegate.scrollTop(true);
              });

              q.resolve();
              cb && cb();
            });

            return q.promise;
          }
        };

        return self;
      }]
    };

  });

  /**
   * @ngdoc service
   * @name $ionicPopover
   * @module ionic
   * @description
   *
   * Related: {@link ionic.controller:ionicPopover ionicPopover controller}.
   *
   * The Popover is a view that floats above an app’s content. Popovers provide an
   * easy way to present or gather information from the user and are
   * commonly used in the following situations:
   *
   * - Show more info about the current view
   * - Select a commonly used tool or configuration
   * - Present a list of actions to perform inside one of your views
   *
   * Put the content of the popover inside of an `<ion-popover-view>` element.
   *
   * @usage
   * ```html
   * <p>
   *   <button ng-click="openPopover($event)">Open Popover</button>
   * </p>
   *
   * <script id="my-popover.html" type="text/ng-template">
   *   <ion-popover-view>
   *     <ion-header-bar>
   *       <h1 class="title">My Popover Title</h1>
   *     </ion-header-bar>
   *     <ion-content>
   *       Hello!
   *     </ion-content>
   *   </ion-popover-view>
   * </script>
   * ```
   * ```js
   * angular.module('testApp', ['ionic'])
   * .controller('MyController', function($scope, $ionicPopover) {
   *
   *   // .fromTemplate() method
   *   var template = '<ion-popover-view><ion-header-bar> <h1 class="title">My Popover Title</h1> </ion-header-bar> <ion-content> Hello! </ion-content></ion-popover-view>';
   *
   *   $scope.popover = $ionicPopover.fromTemplate(template, {
   *     scope: $scope
   *   });
   *
   *   // .fromTemplateUrl() method
   *   $ionicPopover.fromTemplateUrl('my-popover.html', {
   *     scope: $scope
   *   }).then(function(popover) {
   *     $scope.popover = popover;
   *   });
   *
   *
   *   $scope.openPopover = function($event) {
   *     $scope.popover.show($event);
   *   };
   *   $scope.closePopover = function() {
   *     $scope.popover.hide();
   *   };
   *   //Cleanup the popover when we're done with it!
   *   $scope.$on('$destroy', function() {
   *     $scope.popover.remove();
   *   });
   *   // Execute action on hidden popover
   *   $scope.$on('popover.hidden', function() {
   *     // Execute action
   *   });
   *   // Execute action on remove popover
   *   $scope.$on('popover.removed', function() {
   *     // Execute action
   *   });
   * });
   * ```
   */


  IonicModule
  .factory('$ionicPopover', ['$ionicModal', '$ionicPosition', '$document', '$window',
    function ($ionicModal, $ionicPosition, $document, $window) {

      var POPOVER_BODY_PADDING = 6;

      var POPOVER_OPTIONS = {
        viewType: 'popover',
        hideDelay: 1,
        animation: 'none',
        positionView: positionView
      };

      function positionView(target, popoverEle) {
        var targetEle = jqLite(target.target || target);
        var buttonOffset = $ionicPosition.offset(targetEle);
        var popoverWidth = popoverEle.prop('offsetWidth');
        var popoverHeight = popoverEle.prop('offsetHeight');
        // Use innerWidth and innerHeight, because clientWidth and clientHeight
        // doesn't work consistently for body on all platforms
        var bodyWidth = $window.innerWidth;
        var bodyHeight = $window.innerHeight;

        var popoverCSS = {
          left: buttonOffset.left + buttonOffset.width / 2 - popoverWidth / 2
        };
        var arrowEle = jqLite(popoverEle[0].querySelector('.popover-arrow'));

        if (popoverCSS.left < POPOVER_BODY_PADDING) {
          popoverCSS.left = POPOVER_BODY_PADDING;
        } else if (popoverCSS.left + popoverWidth + POPOVER_BODY_PADDING > bodyWidth) {
          popoverCSS.left = bodyWidth - popoverWidth - POPOVER_BODY_PADDING;
        }

        // If the popover when popped down stretches past bottom of screen,
        // make it pop up if there's room above
        if (buttonOffset.top + buttonOffset.height + popoverHeight > bodyHeight &&
          buttonOffset.top - popoverHeight > 0) {
          popoverCSS.top = buttonOffset.top - popoverHeight;
          popoverEle.addClass('popover-bottom');
        } else {
          popoverCSS.top = buttonOffset.top + buttonOffset.height;
          popoverEle.removeClass('popover-bottom');
        }

        arrowEle.css({
          left: buttonOffset.left + buttonOffset.width / 2 -
            arrowEle.prop('offsetWidth') / 2 - popoverCSS.left + 'px'
        });

        popoverEle.css({
          top: popoverCSS.top + 'px',
          left: popoverCSS.left + 'px',
          marginLeft: '0',
          opacity: '1'
        });

      }

      /**
       * @ngdoc controller
       * @name ionicPopover
       * @module ionic
       * @description
       * Instantiated by the {@link ionic.service:$ionicPopover} service.
       *
       * Be sure to call [remove()](#remove) when you are done with each popover
       * to clean it up and avoid memory leaks.
       *
       * Note: a popover will broadcast 'popover.shown', 'popover.hidden', and 'popover.removed' events from its originating
       * scope, passing in itself as an event argument. Both the popover.removed and popover.hidden events are
       * called when the popover is removed.
       */

      /**
       * @ngdoc method
       * @name ionicPopover#initialize
       * @description Creates a new popover controller instance.
       * @param {object} options An options object with the following properties:
       *  - `{object=}` `scope` The scope to be a child of.
       *    Default: creates a child of $rootScope.
       *  - `{boolean=}` `focusFirstInput` Whether to autofocus the first input of
       *    the popover when shown.  Default: false.
       *  - `{boolean=}` `backdropClickToClose` Whether to close the popover on clicking the backdrop.
       *    Default: true.
       *  - `{boolean=}` `hardwareBackButtonClose` Whether the popover can be closed using the hardware
       *    back button on Android and similar devices.  Default: true.
       */

      /**
       * @ngdoc method
       * @name ionicPopover#show
       * @description Show this popover instance.
       * @param {$event} $event The $event or target element which the popover should align
       * itself next to.
       * @returns {promise} A promise which is resolved when the popover is finished animating in.
       */

      /**
       * @ngdoc method
       * @name ionicPopover#hide
       * @description Hide this popover instance.
       * @returns {promise} A promise which is resolved when the popover is finished animating out.
       */

      /**
       * @ngdoc method
       * @name ionicPopover#remove
       * @description Remove this popover instance from the DOM and clean up.
       * @returns {promise} A promise which is resolved when the popover is finished animating out.
       */

      /**
       * @ngdoc method
       * @name ionicPopover#isShown
       * @returns boolean Whether this popover is currently shown.
       */

      return {
        /**
         * @ngdoc method
         * @name $ionicPopover#fromTemplate
         * @param {string} templateString The template string to use as the popovers's
         * content.
         * @param {object} options Options to be passed to the initialize method.
         * @returns {object} An instance of an {@link ionic.controller:ionicPopover}
         * controller (ionicPopover is built on top of $ionicPopover).
         */
        fromTemplate: function (templateString, options) {
          return $ionicModal.fromTemplate(templateString, ionic.Utils.extend({}, POPOVER_OPTIONS, options));
        },
        /**
         * @ngdoc method
         * @name $ionicPopover#fromTemplateUrl
         * @param {string} templateUrl The url to load the template from.
         * @param {object} options Options to be passed to the initialize method.
         * @returns {promise} A promise that will be resolved with an instance of
         * an {@link ionic.controller:ionicPopover} controller (ionicPopover is built on top of $ionicPopover).
         */
        fromTemplateUrl: function (url, options) {
          return $ionicModal.fromTemplateUrl(url, ionic.Utils.extend({}, POPOVER_OPTIONS, options));
        }
      };

    }]);


  var POPUP_TPL =
    '<div class="popup-container" ng-class="cssClass">' +
    '<div class="popup">' +
    '<div class="popup-head">' +
    '<h3 class="popup-title" ng-bind-html="title"></h3>' +
    '<h5 class="popup-sub-title" ng-bind-html="subTitle" ng-if="subTitle"></h5>' +
    '</div>' +
    '<div class="popup-body">' +
    '</div>' +
    '<div class="popup-buttons" ng-show="buttons.length">' +
    '<button ng-repeat="button in buttons" ng-click="$buttonTapped(button, $event)" class="button" ng-class="button.type || \'button-default\'" ng-bind-html="button.text"></button>' +
    '</div>' +
    '</div>' +
    '</div>';

  /**
   * @ngdoc service
   * @name $ionicPopup
   * @module ionic
   * @restrict E
   * @codepen zkmhJ
   * @description
   *
   * The Ionic Popup service allows programmatically creating and showing popup
   * windows that require the user to respond in order to continue.
   *
   * The popup system has support for more flexible versions of the built in `alert()`, `prompt()`,
   * and `confirm()` functions that users are used to, in addition to allowing popups with completely
   * custom content and look.
   *
   * An input can be given an `autofocus` attribute so it automatically receives focus when
   * the popup first shows. However, depending on certain use-cases this can cause issues with
   * the tap/click system, which is why Ionic prefers using the `autofocus` attribute as
   * an opt-in feature and not the default.
   *
   * @usage
   * A few basic examples, see below for details about all of the options available.
   *
   * ```js
   *angular.module('mySuperApp', ['ionic'])
   *.controller('PopupCtrl',function($scope, $ionicPopup, $timeout) {
   *
   * // Triggered on a button click, or some other target
   * $scope.showPopup = function() {
   *   $scope.data = {};
   *
   *   // An elaborate, custom popup
   *   var myPopup = $ionicPopup.show({
   *     template: '<input type="password" ng-model="data.wifi">',
   *     title: 'Enter Wi-Fi Password',
   *     subTitle: 'Please use normal things',
   *     scope: $scope,
   *     buttons: [
   *       { text: 'Cancel' },
   *       {
   *         text: '<b>Save</b>',
   *         type: 'button-positive',
   *         onTap: function(e) {
   *           if (!$scope.data.wifi) {
   *             //don't allow the user to close unless he enters wifi password
   *             e.preventDefault();
   *           } else {
   *             return $scope.data.wifi;
   *           }
   *         }
   *       }
   *     ]
   *   });
   *
   *   myPopup.then(function(res) {
   *     console.log('Tapped!', res);
   *   });
   *
   *   $timeout(function() {
   *      myPopup.close(); //close the popup after 3 seconds for some reason
   *   }, 3000);
   *  };
   *
   *  // A confirm dialog
   *  $scope.showConfirm = function() {
   *    var confirmPopup = $ionicPopup.confirm({
   *      title: 'Consume Ice Cream',
   *      template: 'Are you sure you want to eat this ice cream?'
   *    });
   *
   *    confirmPopup.then(function(res) {
   *      if(res) {
   *        console.log('You are sure');
   *      } else {
   *        console.log('You are not sure');
   *      }
   *    });
   *  };
   *
   *  // An alert dialog
   *  $scope.showAlert = function() {
   *    var alertPopup = $ionicPopup.alert({
   *      title: 'Don\'t eat that!',
   *      template: 'It might taste good'
   *    });
   *
   *    alertPopup.then(function(res) {
   *      console.log('Thank you for not eating my delicious ice cream cone');
   *    });
   *  };
   *});
   *```
   */

  IonicModule
  .factory('$ionicPopup', [
    '$ionicTemplateLoader',
    '$ionicBackdrop',
    '$q',
    '$timeout',
    '$rootScope',
    '$ionicBody',
    '$compile',
    '$ionicPlatform',
    '$ionicModal',
    'IONIC_BACK_PRIORITY',
    function ($ionicTemplateLoader, $ionicBackdrop, $q, $timeout, $rootScope, $ionicBody, $compile, $ionicPlatform, $ionicModal, IONIC_BACK_PRIORITY) {
      //TODO allow this to be configured
      var config = {
        stackPushDelay: 75
      };
      var popupStack = [];

      var $ionicPopup = {
        /**
         * @ngdoc method
         * @description
         * Show a complex popup. This is the master show function for all popups.
         *
         * A complex popup has a `buttons` array, with each button having a `text` and `type`
         * field, in addition to an `onTap` function.  The `onTap` function, called when
         * the corresponding button on the popup is tapped, will by default close the popup
         * and resolve the popup promise with its return value.  If you wish to prevent the
         * default and keep the popup open on button tap, call `event.preventDefault()` on the
         * passed in tap event.  Details below.
         *
         * @name $ionicPopup#show
         * @param {object} options The options for the new popup, of the form:
         *
         * ```
         * {
         *   title: '', // String. The title of the popup.
         *   cssClass: '', // String, The custom CSS class name
         *   subTitle: '', // String (optional). The sub-title of the popup.
         *   template: '', // String (optional). The html template to place in the popup body.
         *   templateUrl: '', // String (optional). The URL of an html template to place in the popup   body.
         *   scope: null, // Scope (optional). A scope to link to the popup content.
         *   buttons: [{ // Array[Object] (optional). Buttons to place in the popup footer.
         *     text: 'Cancel',
         *     type: 'button-default',
         *     onTap: function(e) {
         *       // e.preventDefault() will stop the popup from closing when tapped.
         *       e.preventDefault();
         *     }
         *   }, {
         *     text: 'OK',
         *     type: 'button-positive',
         *     onTap: function(e) {
         *       // Returning a value will cause the promise to resolve with the given value.
         *       return scope.data.response;
         *     }
         *   }]
         * }
         * ```
         *
         * @returns {object} A promise which is resolved when the popup is closed. Has an additional
         * `close` function, which can be used to programmatically close the popup.
         */
        show: showPopup,

        /**
         * @ngdoc method
         * @name $ionicPopup#alert
         * @description Show a simple alert popup with a message and one button that the user can
         * tap to close the popup.
         *
         * @param {object} options The options for showing the alert, of the form:
         *
         * ```
         * {
         *   title: '', // String. The title of the popup.
         *   cssClass: '', // String, The custom CSS class name
         *   subTitle: '', // String (optional). The sub-title of the popup.
         *   template: '', // String (optional). The html template to place in the popup body.
         *   templateUrl: '', // String (optional). The URL of an html template to place in the popup   body.
         *   okText: '', // String (default: 'OK'). The text of the OK button.
         *   okType: '', // String (default: 'button-positive'). The type of the OK button.
         * }
         * ```
         *
         * @returns {object} A promise which is resolved when the popup is closed. Has one additional
         * function `close`, which can be called with any value to programmatically close the popup
         * with the given value.
         */
        alert: showAlert,

        /**
         * @ngdoc method
         * @name $ionicPopup#confirm
         * @description
         * Show a simple confirm popup with a Cancel and OK button.
         *
         * Resolves the promise with true if the user presses the OK button, and false if the
         * user presses the Cancel button.
         *
         * @param {object} options The options for showing the confirm popup, of the form:
         *
         * ```
         * {
         *   title: '', // String. The title of the popup.
         *   cssClass: '', // String, The custom CSS class name
         *   subTitle: '', // String (optional). The sub-title of the popup.
         *   template: '', // String (optional). The html template to place in the popup body.
         *   templateUrl: '', // String (optional). The URL of an html template to place in the popup   body.
         *   cancelText: '', // String (default: 'Cancel'). The text of the Cancel button.
         *   cancelType: '', // String (default: 'button-default'). The type of the Cancel button.
         *   okText: '', // String (default: 'OK'). The text of the OK button.
         *   okType: '', // String (default: 'button-positive'). The type of the OK button.
         * }
         * ```
         *
         * @returns {object} A promise which is resolved when the popup is closed. Has one additional
         * function `close`, which can be called with any value to programmatically close the popup
         * with the given value.
         */
        confirm: showConfirm,

        /**
         * @ngdoc method
         * @name $ionicPopup#prompt
         * @description Show a simple prompt popup, which has an input, OK button, and Cancel button.
         * Resolves the promise with the value of the input if the user presses OK, and with undefined
         * if the user presses Cancel.
         *
         * ```javascript
         *  $ionicPopup.prompt({
         *    title: 'Password Check',
         *    template: 'Enter your secret password',
         *    inputType: 'password',
         *    inputPlaceholder: 'Your password'
         *  }).then(function(res) {
         *    console.log('Your password is', res);
         *  });
         * ```
         * @param {object} options The options for showing the prompt popup, of the form:
         *
         * ```
         * {
         *   title: '', // String. The title of the popup.
         *   cssClass: '', // String, The custom CSS class name
         *   subTitle: '', // String (optional). The sub-title of the popup.
         *   template: '', // String (optional). The html template to place in the popup body.
         *   templateUrl: '', // String (optional). The URL of an html template to place in the popup body.
         *   inputType: // String (default: 'text'). The type of input to use
         *   defaultText: // String (default: ''). The initial value placed into the input.
         *   maxLength: // Integer (default: null). Specify a maxlength attribute for the input.
         *   inputPlaceholder: // String (default: ''). A placeholder to use for the input.
         *   cancelText: // String (default: 'Cancel'. The text of the Cancel button.
         *   cancelType: // String (default: 'button-default'). The type of the Cancel button.
         *   okText: // String (default: 'OK'). The text of the OK button.
         *   okType: // String (default: 'button-positive'). The type of the OK button.
         * }
         * ```
         *
         * @returns {object} A promise which is resolved when the popup is closed. Has one additional
         * function `close`, which can be called with any value to programmatically close the popup
         * with the given value.
         */
        prompt: showPrompt,
        /**
         * @private for testing
         */
        _createPopup: createPopup,
        _popupStack: popupStack
      };

      return $ionicPopup;

      function createPopup(options) {
        options = extend({
          scope: null,
          title: '',
          buttons: []
        }, options || {});

        var self = {};
        self.scope = (options.scope || $rootScope).$new();
        self.element = jqLite(POPUP_TPL);
        self.responseDeferred = $q.defer();

        $ionicBody.get().appendChild(self.element[0]);
        $compile(self.element)(self.scope);

        extend(self.scope, {
          title: options.title,
          buttons: options.buttons,
          subTitle: options.subTitle,
          cssClass: options.cssClass,
          $buttonTapped: function (button, event) {
            var result = (button.onTap || noop).apply(self, [event]);
            event = event.originalEvent || event; //jquery events

            if (!event.defaultPrevented) {
              self.responseDeferred.resolve(result);
            }
          }
        });

        $q.when(
          options.templateUrl ?
            $ionicTemplateLoader.load(options.templateUrl) :
            (options.template || options.content || '')
        ).then(function (template) {
          var popupBody = jqLite(self.element[0].querySelector('.popup-body'));
          if (template) {
            popupBody.html(template);
            $compile(popupBody.contents())(self.scope);
          } else {
            popupBody.remove();
          }
        });

        self.show = function () {
          if (self.isShown || self.removed) return;

          $ionicModal.stack.add(self);
          self.isShown = true;
          ionic.requestAnimationFrame(function () {
            //if hidden while waiting for raf, don't show
            if (!self.isShown) return;

            self.element.removeClass('popup-hidden');
            self.element.addClass('popup-showing active');
            focusInput(self.element);
          });
        };

        self.hide = function (callback) {
          callback = callback || noop;
          if (!self.isShown) return callback();

          $ionicModal.stack.remove(self);
          self.isShown = false;
          self.element.removeClass('active');
          self.element.addClass('popup-hidden');
          $timeout(callback, 250, false);
        };

        self.remove = function () {
          if (self.removed) return;

          self.hide(function () {
            self.element.remove();
            self.scope.$destroy();
          });

          self.removed = true;
        };

        return self;
      }

      function onHardwareBackButton() {
        var last = popupStack[popupStack.length - 1];
        last && last.responseDeferred.resolve();
      }

      function showPopup(options) {
        var popup = $ionicPopup._createPopup(options);
        var showDelay = 0;

        if (popupStack.length > 0) {
          showDelay = config.stackPushDelay;
          $timeout(popupStack[popupStack.length - 1].hide, showDelay, false);
        } else {
          //Add popup-open & backdrop if this is first popup
          $ionicBody.addClass('popup-open');
          $ionicBackdrop.retain();
          //only show the backdrop on the first popup
          $ionicPopup._backButtonActionDone = $ionicPlatform.registerBackButtonAction(
            onHardwareBackButton,
            IONIC_BACK_PRIORITY.popup
          );
        }

        // Expose a 'close' method on the returned promise
        popup.responseDeferred.promise.close = function popupClose(result) {
          if (!popup.removed) popup.responseDeferred.resolve(result);
        };
        //DEPRECATED: notify the promise with an object with a close method
        popup.responseDeferred.notify({close: popup.responseDeferred.close});

        doShow();

        return popup.responseDeferred.promise;

        function doShow() {
          popupStack.push(popup);
          $timeout(popup.show, showDelay, false);

          popup.responseDeferred.promise.then(function (result) {
            var index = popupStack.indexOf(popup);
            if (index !== -1) {
              popupStack.splice(index, 1);
            }

            popup.remove();

            if (popupStack.length > 0) {
              popupStack[popupStack.length - 1].show();
            } else {
              $ionicBackdrop.release();
              //Remove popup-open & backdrop if this is last popup
              $timeout(function () {
                // wait to remove this due to a 300ms delay native
                // click which would trigging whatever was underneath this
                if (!popupStack.length) {
                  $ionicBody.removeClass('popup-open');
                }
              }, 400, false);
              ($ionicPopup._backButtonActionDone || noop)();
            }


            return result;
          });

        }

      }

      function focusInput(element) {
        var focusOn = element[0].querySelector('[autofocus]');
        if (focusOn) {
          focusOn.focus();
        }
      }

      function showAlert(opts) {
        return showPopup(extend({
          buttons: [{
            text: opts.okText || 'OK',
            type: opts.okType || 'button-positive',
            onTap: function () {
              return true;
            }
          }]
        }, opts || {}));
      }

      function showConfirm(opts) {
        return showPopup(extend({
          buttons: [{
            text: opts.cancelText || 'Cancel',
            type: opts.cancelType || 'button-default',
            onTap: function () {
              return false;
            }
          }, {
            text: opts.okText || 'OK',
            type: opts.okType || 'button-positive',
            onTap: function () {
              return true;
            }
          }]
        }, opts || {}));
      }

      function showPrompt(opts) {
        var scope = $rootScope.$new(true);
        scope.data = {};
        scope.data.fieldtype = opts.inputType ? opts.inputType : 'text';
        scope.data.response = opts.defaultText ? opts.defaultText : '';
        scope.data.placeholder = opts.inputPlaceholder ? opts.inputPlaceholder : '';
        scope.data.maxlength = opts.maxLength ? parseInt(opts.maxLength) : '';
        var text = '';
        if (opts.template && /<[a-z][\s\S]*>/i.test(opts.template) === false) {
          text = '<span>' + opts.template + '</span>';
          delete opts.template;
        }
        return showPopup(extend({
          template: text + '<input ng-model="data.response" '
            + 'type="{{ data.fieldtype }}"'
            + 'maxlength="{{ data.maxlength }}"'
            + 'placeholder="{{ data.placeholder }}"'
            + '>',
          scope: scope,
          buttons: [{
            text: opts.cancelText || 'Cancel',
            type: opts.cancelType || 'button-default',
            onTap: function () {
            }
          }, {
            text: opts.okText || 'OK',
            type: opts.okType || 'button-positive',
            onTap: function () {
              return scope.data.response || '';
            }
          }]
        }, opts || {}));
      }
    }]);

  /**
   * @ngdoc service
   * @name $ionicPosition
   * @module ionic
   * @description
   * A set of utility methods that can be use to retrieve position of DOM elements.
   * It is meant to be used where we need to absolute-position DOM elements in
   * relation to other, existing elements (this is the case for tooltips, popovers, etc.).
   *
   * Adapted from [AngularUI Bootstrap](https://github.com/angular-ui/bootstrap/blob/master/src/position/position.js),
   * ([license](https://github.com/angular-ui/bootstrap/blob/master/LICENSE))
   */
  IonicModule
  .factory('$ionicPosition', ['$document', '$window', function ($document, $window) {

    function getStyle(el, cssprop) {
      if (el.currentStyle) { //IE
        return el.currentStyle[cssprop];
      } else if ($window.getComputedStyle) {
        return $window.getComputedStyle(el)[cssprop];
      }
      // finally try and get inline style
      return el.style[cssprop];
    }

    /**
     * Checks if a given element is statically positioned
     * @param element - raw DOM element
     */
    function isStaticPositioned(element) {
      return (getStyle(element, 'position') || 'static') === 'static';
    }

    /**
     * returns the closest, non-statically positioned parentOffset of a given element
     * @param element
     */
    var parentOffsetEl = function (element) {
      var docDomEl = $document[0];
      var offsetParent = element.offsetParent || docDomEl;
      while (offsetParent && offsetParent !== docDomEl && isStaticPositioned(offsetParent)) {
        offsetParent = offsetParent.offsetParent;
      }
      return offsetParent || docDomEl;
    };

    return {
      /**
       * @ngdoc method
       * @name $ionicPosition#position
       * @description Get the current coordinates of the element, relative to the offset parent.
       * Read-only equivalent of [jQuery's position function](http://api.jquery.com/position/).
       * @param {element} element The element to get the position of.
       * @returns {object} Returns an object containing the properties top, left, width and height.
       */
      position: function (element) {
        var elBCR = this.offset(element);
        var offsetParentBCR = {top: 0, left: 0};
        var offsetParentEl = parentOffsetEl(element[0]);
        if (offsetParentEl != $document[0]) {
          offsetParentBCR = this.offset(jqLite(offsetParentEl));
          offsetParentBCR.top += offsetParentEl.clientTop - offsetParentEl.scrollTop;
          offsetParentBCR.left += offsetParentEl.clientLeft - offsetParentEl.scrollLeft;
        }

        var boundingClientRect = element[0].getBoundingClientRect();
        return {
          width: boundingClientRect.width || element.prop('offsetWidth'),
          height: boundingClientRect.height || element.prop('offsetHeight'),
          top: elBCR.top - offsetParentBCR.top,
          left: elBCR.left - offsetParentBCR.left
        };
      },

      /**
       * @ngdoc method
       * @name $ionicPosition#offset
       * @description Get the current coordinates of the element, relative to the document.
       * Read-only equivalent of [jQuery's offset function](http://api.jquery.com/offset/).
       * @param {element} element The element to get the offset of.
       * @returns {object} Returns an object containing the properties top, left, width and height.
       */
      offset: function (element) {
        var boundingClientRect = element[0].getBoundingClientRect();
        return {
          width: boundingClientRect.width || element.prop('offsetWidth'),
          height: boundingClientRect.height || element.prop('offsetHeight'),
          top: boundingClientRect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
          left: boundingClientRect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
        };
      }

    };
  }]);


  /**
   * @ngdoc service
   * @name $ionicScrollDelegate
   * @module ionic
   * @description
   * Delegate for controlling scrollViews (created by
   * {@link ionic.directive:ionContent} and
   * {@link ionic.directive:ionScroll} directives).
   *
   * Methods called directly on the $ionicScrollDelegate service will control all scroll
   * views.  Use the {@link ionic.service:$ionicScrollDelegate#$getByHandle $getByHandle}
   * method to control specific scrollViews.
   *
   * @usage
   *
   * ```html
   * <body ng-controller="MainCtrl">
   *   <ion-content>
   *     <button ng-click="scrollTop()">Scroll to Top!</button>
   *   </ion-content>
   * </body>
   * ```
   * ```js
   * function MainCtrl($scope, $ionicScrollDelegate) {
   *   $scope.scrollTop = function() {
   *     $ionicScrollDelegate.scrollTop();
   *   };
   * }
   * ```
   *
   * Example of advanced usage, with two scroll areas using `delegate-handle`
   * for fine control.
   *
   * ```html
   * <body ng-controller="MainCtrl">
   *   <ion-content delegate-handle="mainScroll">
   *     <button ng-click="scrollMainToTop()">
   *       Scroll content to top!
   *     </button>
   *     <ion-scroll delegate-handle="small" style="height: 100px;">
   *       <button ng-click="scrollSmallToTop()">
   *         Scroll small area to top!
   *       </button>
   *     </ion-scroll>
   *   </ion-content>
   * </body>
   * ```
   * ```js
   * function MainCtrl($scope, $ionicScrollDelegate) {
   *   $scope.scrollMainToTop = function() {
   *     $ionicScrollDelegate.$getByHandle('mainScroll').scrollTop();
   *   };
   *   $scope.scrollSmallToTop = function() {
   *     $ionicScrollDelegate.$getByHandle('small').scrollTop();
   *   };
   * }
   * ```
   */
  IonicModule
  .service('$ionicScrollDelegate', ionic.DelegateService([
    /**
     * @ngdoc method
     * @name $ionicScrollDelegate#resize
     * @description Tell the scrollView to recalculate the size of its container.
     */
    'resize',
    /**
     * @ngdoc method
     * @name $ionicScrollDelegate#scrollTop
     * @param {boolean=} shouldAnimate Whether the scroll should animate.
     */
    'scrollTop',
    /**
     * @ngdoc method
     * @name $ionicScrollDelegate#scrollBottom
     * @param {boolean=} shouldAnimate Whether the scroll should animate.
     */
    'scrollBottom',
    /**
     * @ngdoc method
     * @name $ionicScrollDelegate#scrollTo
     * @param {number} left The x-value to scroll to.
     * @param {number} top The y-value to scroll to.
     * @param {boolean=} shouldAnimate Whether the scroll should animate.
     */
    'scrollTo',
    /**
     * @ngdoc method
     * @name $ionicScrollDelegate#scrollBy
     * @param {number} left The x-offset to scroll by.
     * @param {number} top The y-offset to scroll by.
     * @param {boolean=} shouldAnimate Whether the scroll should animate.
     */
    'scrollBy',
    /**
     * @ngdoc method
     * @name $ionicScrollDelegate#zoomTo
     * @param {number} level Level to zoom to.
     * @param {boolean=} animate Whether to animate the zoom.
     * @param {number=} originLeft Zoom in at given left coordinate.
     * @param {number=} originTop Zoom in at given top coordinate.
     */
    'zoomTo',
    /**
     * @ngdoc method
     * @name $ionicScrollDelegate#zoomBy
     * @param {number} factor The factor to zoom by.
     * @param {boolean=} animate Whether to animate the zoom.
     * @param {number=} originLeft Zoom in at given left coordinate.
     * @param {number=} originTop Zoom in at given top coordinate.
     */
    'zoomBy',
    /**
     * @ngdoc method
     * @name $ionicScrollDelegate#getScrollPosition
     * @returns {object} The scroll position of this view, with the following properties:
     *  - `{number}` `left` The distance the user has scrolled from the left (starts at 0).
     *  - `{number}` `top` The distance the user has scrolled from the top (starts at 0).
     *  - `{number}` `zoom` The current zoom level.
     */
    'getScrollPosition',
    /**
     * @ngdoc method
     * @name $ionicScrollDelegate#anchorScroll
     * @description Tell the scrollView to scroll to the element with an id
     * matching window.location.hash.
     *
     * If no matching element is found, it will scroll to top.
     *
     * @param {boolean=} shouldAnimate Whether the scroll should animate.
     */
    'anchorScroll',
    /**
     * @ngdoc method
     * @name $ionicScrollDelegate#freezeScroll
     * @description Does not allow this scroll view to scroll either x or y.
     * @param {boolean=} shouldFreeze Should this scroll view be prevented from scrolling or not.
     * @returns {boolean} If the scroll view is being prevented from scrolling or not.
     */
    'freezeScroll',
    /**
     * @ngdoc method
     * @name $ionicScrollDelegate#freezeAllScrolls
     * @description Does not allow any of the app's scroll views to scroll either x or y.
     * @param {boolean=} shouldFreeze Should all app scrolls be prevented from scrolling or not.
     */
    'freezeAllScrolls',
    /**
     * @ngdoc method
     * @name $ionicScrollDelegate#getScrollView
     * @returns {object} The scrollView associated with this delegate.
     */
    'getScrollView'
    /**
     * @ngdoc method
     * @name $ionicScrollDelegate#$getByHandle
     * @param {string} handle
     * @returns `delegateInstance` A delegate instance that controls only the
     * scrollViews with `delegate-handle` matching the given handle.
     *
     * Example: `$ionicScrollDelegate.$getByHandle('my-handle').scrollTop();`
     */
  ]));


  /**
   * @ngdoc service
   * @name $ionicSideMenuDelegate
   * @module ionic
   *
   * @description
   * Delegate for controlling the {@link ionic.directive:ionSideMenus} directive.
   *
   * Methods called directly on the $ionicSideMenuDelegate service will control all side
   * menus.  Use the {@link ionic.service:$ionicSideMenuDelegate#$getByHandle $getByHandle}
   * method to control specific ionSideMenus instances.
   *
   * @usage
   *
   * ```html
   * <body ng-controller="MainCtrl">
   *   <ion-side-menus>
   *     <ion-side-menu-content>
   *       Content!
   *       <button ng-click="toggleLeftSideMenu()">
   *         Toggle Left Side Menu
   *       </button>
   *     </ion-side-menu-content>
   *     <ion-side-menu side="left">
   *       Left Menu!
   *     <ion-side-menu>
   *   </ion-side-menus>
   * </body>
   * ```
   * ```js
   * function MainCtrl($scope, $ionicSideMenuDelegate) {
   *   $scope.toggleLeftSideMenu = function() {
   *     $ionicSideMenuDelegate.toggleLeft();
   *   };
   * }
   * ```
   */
  IonicModule
  .service('$ionicSideMenuDelegate', ionic.DelegateService([
    /**
     * @ngdoc method
     * @name $ionicSideMenuDelegate#toggleLeft
     * @description Toggle the left side menu (if it exists).
     * @param {boolean=} isOpen Whether to open or close the menu.
     * Default: Toggles the menu.
     */
    'toggleLeft',
    /**
     * @ngdoc method
     * @name $ionicSideMenuDelegate#toggleRight
     * @description Toggle the right side menu (if it exists).
     * @param {boolean=} isOpen Whether to open or close the menu.
     * Default: Toggles the menu.
     */
    'toggleRight',
    /**
     * @ngdoc method
     * @name $ionicSideMenuDelegate#getOpenRatio
     * @description Gets the ratio of open amount over menu width. For example, a
     * menu of width 100 that is opened by 50 pixels is 50% opened, and would return
     * a ratio of 0.5.
     *
     * @returns {float} 0 if nothing is open, between 0 and 1 if left menu is
     * opened/opening, and between 0 and -1 if right menu is opened/opening.
     */
    'getOpenRatio',
    /**
     * @ngdoc method
     * @name $ionicSideMenuDelegate#isOpen
     * @returns {boolean} Whether either the left or right menu is currently opened.
     */
    'isOpen',
    /**
     * @ngdoc method
     * @name $ionicSideMenuDelegate#isOpenLeft
     * @returns {boolean} Whether the left menu is currently opened.
     */
    'isOpenLeft',
    /**
     * @ngdoc method
     * @name $ionicSideMenuDelegate#isOpenRight
     * @returns {boolean} Whether the right menu is currently opened.
     */
    'isOpenRight',
    /**
     * @ngdoc method
     * @name $ionicSideMenuDelegate#canDragContent
     * @param {boolean=} canDrag Set whether the content can or cannot be dragged to open
     * side menus.
     * @returns {boolean} Whether the content can be dragged to open side menus.
     */
    'canDragContent',
    /**
     * @ngdoc method
     * @name $ionicSideMenuDelegate#edgeDragThreshold
     * @param {boolean|number=} value Set whether the content drag can only start if it is below a certain threshold distance from the edge of the screen. Accepts three different values:
     *  - If a non-zero number is given, that many pixels is used as the maximum allowed distance from the edge that starts dragging the side menu.
     *  - If true is given, the default number of pixels (25) is used as the maximum allowed distance.
     *  - If false or 0 is given, the edge drag threshold is disabled, and dragging from anywhere on the content is allowed.
     * @returns {boolean} Whether the drag can start only from within the edge of screen threshold.
     */
    'edgeDragThreshold'
    /**
     * @ngdoc method
     * @name $ionicSideMenuDelegate#$getByHandle
     * @param {string} handle
     * @returns `delegateInstance` A delegate instance that controls only the
     * {@link ionic.directive:ionSideMenus} directives with `delegate-handle` matching
     * the given handle.
     *
     * Example: `$ionicSideMenuDelegate.$getByHandle('my-handle').toggleLeft();`
     */
  ]));


  /**
   * @ngdoc service
   * @name $ionicSlideBoxDelegate
   * @module ionic
   * @description
   * Delegate that controls the {@link ionic.directive:ionSlideBox} directive.
   *
   * Methods called directly on the $ionicSlideBoxDelegate service will control all slide boxes.  Use the {@link ionic.service:$ionicSlideBoxDelegate#$getByHandle $getByHandle}
   * method to control specific slide box instances.
   *
   * @usage
   *
   * ```html
   * <ion-view>
   *   <ion-slide-box>
   *     <ion-slide>
   *       <div class="box blue">
   *         <button ng-click="nextSlide()">Next slide!</button>
   *       </div>
   *     </ion-slide>
   *     <ion-slide>
   *       <div class="box red">
   *         Slide 2!
   *       </div>
   *     </ion-slide>
   *   </ion-slide-box>
   * </ion-view>
   * ```
   * ```js
   * function MyCtrl($scope, $ionicSlideBoxDelegate) {
   *   $scope.nextSlide = function() {
   *     $ionicSlideBoxDelegate.next();
   *   }
   * }
   * ```
   */
  IonicModule
  .service('$ionicSlideBoxDelegate', ionic.DelegateService([
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#update
     * @description
     * Update the slidebox (for example if using Angular with ng-repeat,
     * resize it for the elements inside).
     */
    'update',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#slide
     * @param {number} to The index to slide to.
     * @param {number=} speed The number of milliseconds the change should take.
     */
    'slide',
    'select',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#enableSlide
     * @param {boolean=} shouldEnable Whether to enable sliding the slidebox.
     * @returns {boolean} Whether sliding is enabled.
     */
    'enableSlide',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#previous
     * @param {number=} speed The number of milliseconds the change should take.
     * @description Go to the previous slide. Wraps around if at the beginning.
     */
    'previous',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#next
     * @param {number=} speed The number of milliseconds the change should take.
     * @description Go to the next slide. Wraps around if at the end.
     */
    'next',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#stop
     * @description Stop sliding. The slideBox will not move again until
     * explicitly told to do so.
     */
    'stop',
    'autoPlay',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#start
     * @description Start sliding again if the slideBox was stopped.
     */
    'start',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#currentIndex
     * @returns number The index of the current slide.
     */
    'currentIndex',
    'selected',
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#slidesCount
     * @returns number The number of slides there are currently.
     */
    'slidesCount',
    'count',
    'loop'
    /**
     * @ngdoc method
     * @name $ionicSlideBoxDelegate#$getByHandle
     * @param {string} handle
     * @returns `delegateInstance` A delegate instance that controls only the
     * {@link ionic.directive:ionSlideBox} directives with `delegate-handle` matching
     * the given handle.
     *
     * Example: `$ionicSlideBoxDelegate.$getByHandle('my-handle').stop();`
     */
  ]));


  /**
   * @ngdoc service
   * @name $ionicTabsDelegate
   * @module ionic
   *
   * @description
   * Delegate for controlling the {@link ionic.directive:ionTabs} directive.
   *
   * Methods called directly on the $ionicTabsDelegate service will control all ionTabs
   * directives. Use the {@link ionic.service:$ionicTabsDelegate#$getByHandle $getByHandle}
   * method to control specific ionTabs instances.
   *
   * @usage
   *
   * ```html
   * <body ng-controller="MyCtrl">
   *   <ion-tabs>
   *
   *     <ion-tab title="Tab 1">
   *       Hello tab 1!
   *       <button ng-click="selectTabWithIndex(1)">Select tab 2!</button>
   *     </ion-tab>
   *     <ion-tab title="Tab 2">Hello tab 2!</ion-tab>
   *
   *   </ion-tabs>
   * </body>
   * ```
   * ```js
   * function MyCtrl($scope, $ionicTabsDelegate) {
   *   $scope.selectTabWithIndex = function(index) {
   *     $ionicTabsDelegate.select(index);
   *   }
   * }
   * ```
   */
  IonicModule
  .service('$ionicTabsDelegate', ionic.DelegateService([
    /**
     * @ngdoc method
     * @name $ionicTabsDelegate#select
     * @description Select the tab matching the given index.
     *
     * @param {number} index Index of the tab to select.
     */
    'select',
    /**
     * @ngdoc method
     * @name $ionicTabsDelegate#selectedIndex
     * @returns `number` The index of the selected tab, or -1.
     */
    'selectedIndex',
    /**
     * @ngdoc method
     * @name $ionicTabsDelegate#showBar
     * @description
     * Set/get whether the {@link ionic.directive:ionTabs} is shown
     * @param {boolean} show Whether to show the bar.
     * @returns {boolean} Whether the bar is shown.
     */
    'showBar'
    /**
     * @ngdoc method
     * @name $ionicTabsDelegate#$getByHandle
     * @param {string} handle
     * @returns `delegateInstance` A delegate instance that controls only the
     * {@link ionic.directive:ionTabs} directives with `delegate-handle` matching
     * the given handle.
     *
     * Example: `$ionicTabsDelegate.$getByHandle('my-handle').select(0);`
     */
  ]));

// closure to keep things neat
  (function () {
    var templatesToCache = [];

    /**
     * @ngdoc service
     * @name $ionicTemplateCache
     * @module ionic
     * @description A service that preemptively caches template files to eliminate transition flicker and boost performance.
     * @usage
     * State templates are cached automatically, but you can optionally cache other templates.
     *
     * ```js
     * $ionicTemplateCache('myNgIncludeTemplate.html');
     * ```
     *
     * Optionally disable all preemptive caching with the `$ionicConfigProvider` or individual states by setting `prefetchTemplate`
     * in the `$state` definition
     *
     * ```js
     *   angular.module('learn-ionic-main', ['ionic'])
     *   .config(function($stateProvider, $ionicConfigProvider) {
     *
     *     // disable preemptive template caching globally
     *     $ionicConfigProvider.templates.prefetch(false);
     *
     *     // disable individual states
     *     $stateProvider
     *       .state('tabs', {
     *         url: "/tab",
     *         abstract: true,
     *         prefetchTemplate: false,
     *         templateUrl: "tabs-templates/tabs.html"
     *       })
     *       .state('tabs.home', {
     *         url: "/home",
     *         views: {
     *           'home-tab': {
     *             prefetchTemplate: false,
     *             templateUrl: "tabs-templates/home.html",
     *             controller: 'HomeTabCtrl'
     *           }
     *         }
     *       });
     *   });
     * ```
     */
    IonicModule
    .factory('$ionicTemplateCache', [
      '$http',
      '$templateCache',
      '$timeout',
      function ($http, $templateCache, $timeout) {
        var toCache = templatesToCache,
          hasRun;

        function $ionicTemplateCache(templates) {
          if (typeof templates === 'undefined') {
            return run();
          }
          if (isString(templates)) {
            templates = [templates];
          }
          forEach(templates, function (template) {
            toCache.push(template);
          });
          if (hasRun) {
            run();
          }
        }

        // run through methods - internal method
        function run() {
          var template;
          $ionicTemplateCache._runCount++;

          hasRun = true;
          // ignore if race condition already zeroed out array
          if (toCache.length === 0) return;

          var i = 0;
          while (i < 4 && (template = toCache.pop())) {
            // note that inline templates are ignored by this request
            if (isString(template)) $http.get(template, {cache: $templateCache});
            i++;
          }
          // only preload 3 templates a second
          if (toCache.length) {
            $timeout(run, 1000);
          }
        }

        // exposing for testing
        $ionicTemplateCache._runCount = 0;
        // default method
        return $ionicTemplateCache;
      }])

    // Intercepts the $stateprovider.state() command to look for templateUrls that can be cached
    .config([
      '$stateProvider',
      '$ionicConfigProvider',
      function ($stateProvider, $ionicConfigProvider) {
        var stateProviderState = $stateProvider.state;
        $stateProvider.state = function (stateName, definition) {
          // don't even bother if it's disabled. note, another config may run after this, so it's not a catch-all
          if (typeof definition === 'object') {
            var enabled = definition.prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch();
            if (enabled && isString(definition.templateUrl)) templatesToCache.push(definition.templateUrl);
            if (angular.isObject(definition.views)) {
              for (var key in definition.views) {
                enabled = definition.views[key].prefetchTemplate !== false && templatesToCache.length < $ionicConfigProvider.templates.maxPrefetch();
                if (enabled && isString(definition.views[key].templateUrl)) templatesToCache.push(definition.views[key].templateUrl);
              }
            }
          }
          return stateProviderState.call($stateProvider, stateName, definition);
        };
      }])

    // process the templateUrls collected by the $stateProvider, adding them to the cache
    .run(['$ionicTemplateCache', function ($ionicTemplateCache) {
      $ionicTemplateCache();
    }]);

  })();

  IonicModule
  .factory('$ionicTemplateLoader', [
    '$compile',
    '$controller',
    '$http',
    '$q',
    '$rootScope',
    '$templateCache',
    function ($compile, $controller, $http, $q, $rootScope, $templateCache) {

      return {
        load: fetchTemplate,
        compile: loadAndCompile
      };

      function fetchTemplate(url) {
        return $http.get(url, {cache: $templateCache})
        .then(function (response) {
          return response.data && response.data.trim();
        });
      }

      function loadAndCompile(options) {
        options = extend({
          template: '',
          templateUrl: '',
          scope: null,
          controller: null,
          locals: {},
          appendTo: null
        }, options || {});

        var templatePromise = options.templateUrl ?
          this.load(options.templateUrl) :
          $q.when(options.template);

        return templatePromise.then(function (template) {
          var controller;
          var scope = options.scope || $rootScope.$new();

          //Incase template doesn't have just one root element, do this
          var element = jqLite('<div>').html(template).contents();

          if (options.controller) {
            controller = $controller(
              options.controller,
              extend(options.locals, {
                $scope: scope
              })
            );
            element.children().data('$ngControllerController', controller);
          }
          if (options.appendTo) {
            jqLite(options.appendTo).append(element);
          }

          $compile(element)(scope);

          return {
            element: element,
            scope: scope
          };
        });
      }

    }]);

  /**
   * @private
   * DEPRECATED, as of v1.0.0-beta14 -------
   */
  IonicModule
  .factory('$ionicViewService', ['$ionicHistory', '$log', function ($ionicHistory, $log) {

    function warn(oldMethod, newMethod) {
      $log.warn('$ionicViewService' + oldMethod + ' is deprecated, please use $ionicHistory' + newMethod + ' instead: http://ionicframework.com/docs/nightly/api/service/$ionicHistory/');
    }

    warn('', '');

    var methodsMap = {
      getCurrentView: 'currentView',
      getBackView: 'backView',
      getForwardView: 'forwardView',
      getCurrentStateName: 'currentStateName',
      nextViewOptions: 'nextViewOptions',
      clearHistory: 'clearHistory'
    };

    forEach(methodsMap, function (newMethod, oldMethod) {
      methodsMap[oldMethod] = function () {
        warn('.' + oldMethod, '.' + newMethod);
        return $ionicHistory[newMethod].apply(this, arguments);
      };
    });

    return methodsMap;

  }]);

  /**
   * @private
   * TODO document
   */

  IonicModule.factory('$ionicViewSwitcher', [
    '$timeout',
    '$document',
    '$q',
    '$ionicClickBlock',
    '$ionicConfig',
    '$ionicNavBarDelegate',
    function ($timeout, $document, $q, $ionicClickBlock, $ionicConfig, $ionicNavBarDelegate) {

      var TRANSITIONEND_EVENT = 'webkitTransitionEnd transitionend';
      var DATA_NO_CACHE = '$noCache';
      var DATA_DESTROY_ELE = '$destroyEle';
      var DATA_ELE_IDENTIFIER = '$eleId';
      var DATA_VIEW_ACCESSED = '$accessed';
      var DATA_FALLBACK_TIMER = '$fallbackTimer';
      var DATA_VIEW = '$viewData';
      var NAV_VIEW_ATTR = 'nav-view';
      var VIEW_STATUS_ACTIVE = 'active';
      var VIEW_STATUS_CACHED = 'cached';
      var VIEW_STATUS_STAGED = 'stage';

      var transitionCounter = 0;
      var nextTransition, nextDirection;
      ionic.transition = ionic.transition || {};
      ionic.transition.isActive = false;
      var isActiveTimer;
      var cachedAttr = ionic.DomUtil.cachedAttr;
      var transitionPromises = [];
      var defaultTimeout = 1100;

      var ionicViewSwitcher = {

        create: function (navViewCtrl, viewLocals, enteringView, leavingView, renderStart, renderEnd) {
          // get a reference to an entering/leaving element if they exist
          // loop through to see if the view is already in the navViewElement
          var enteringEle, leavingEle;
          var transitionId = ++transitionCounter;
          var alreadyInDom;

          var switcher = {

            init: function (registerData, callback) {
              ionicViewSwitcher.isTransitioning(true);

              switcher.loadViewElements(registerData);

              switcher.render(registerData, function () {
                callback && callback();
              });
            },

            loadViewElements: function (registerData) {
              var x, l, viewEle;
              var viewElements = navViewCtrl.getViewElements();
              var enteringEleIdentifier = getViewElementIdentifier(viewLocals, enteringView);
              var navViewActiveEleId = navViewCtrl.activeEleId();

              for (x = 0, l = viewElements.length; x < l; x++) {
                viewEle = viewElements.eq(x);

                if (viewEle.data(DATA_ELE_IDENTIFIER) === enteringEleIdentifier) {
                  // we found an existing element in the DOM that should be entering the view
                  if (viewEle.data(DATA_NO_CACHE)) {
                    // the existing element should not be cached, don't use it
                    viewEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier + ionic.Utils.nextUid());
                    viewEle.data(DATA_DESTROY_ELE, true);

                  } else {
                    enteringEle = viewEle;
                  }

                } else if (isDefined(navViewActiveEleId) && viewEle.data(DATA_ELE_IDENTIFIER) === navViewActiveEleId) {
                  leavingEle = viewEle;
                }

                if (enteringEle && leavingEle) break;
              }

              alreadyInDom = !!enteringEle;

              if (!alreadyInDom) {
                // still no existing element to use
                // create it using existing template/scope/locals
                enteringEle = registerData.ele || ionicViewSwitcher.createViewEle(viewLocals);

                // existing elements in the DOM are looked up by their state name and state id
                enteringEle.data(DATA_ELE_IDENTIFIER, enteringEleIdentifier);
              }

              if (renderEnd) {
                navViewCtrl.activeEleId(enteringEleIdentifier);
              }

              registerData.ele = null;
            },

            render: function (registerData, callback) {
              if (alreadyInDom) {
                // it was already found in the DOM, just reconnect the scope
                ionic.Utils.reconnectScope(enteringEle.scope());

              } else {
                // the entering element is not already in the DOM
                // set that the entering element should be "staged" and its
                // styles of where this element will go before it hits the DOM
                navViewAttr(enteringEle, VIEW_STATUS_STAGED);

                var enteringData = getTransitionData(viewLocals, enteringEle, registerData.direction, enteringView);
                var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none;
                transitionFn(enteringEle, null, enteringData.direction, true).run(0);

                enteringEle.data(DATA_VIEW, {
                  viewId: enteringData.viewId,
                  historyId: enteringData.historyId,
                  stateName: enteringData.stateName,
                  stateParams: enteringData.stateParams
                });

                // if the current state has cache:false
                // or the element has cache-view="false" attribute
                if (viewState(viewLocals).cache === false || viewState(viewLocals).cache === 'false' ||
                  enteringEle.attr('cache-view') == 'false' || $ionicConfig.views.maxCache() === 0) {
                  enteringEle.data(DATA_NO_CACHE, true);
                }

                // append the entering element to the DOM, create a new scope and run link
                var viewScope = navViewCtrl.appendViewElement(enteringEle, viewLocals);

                delete enteringData.direction;
                delete enteringData.transition;
                viewScope.$emit('$ionicView.loaded', enteringData);
              }

              // update that this view was just accessed
              enteringEle.data(DATA_VIEW_ACCESSED, Date.now());

              callback && callback();
            },

            transition: function (direction, enableBack, allowAnimate) {
              var deferred;
              var enteringData = getTransitionData(viewLocals, enteringEle, direction, enteringView);
              var leavingData = extend(extend({}, enteringData), getViewData(leavingView));
              enteringData.transitionId = leavingData.transitionId = transitionId;
              enteringData.fromCache = !!alreadyInDom;
              enteringData.enableBack = !!enableBack;
              enteringData.renderStart = renderStart;
              enteringData.renderEnd = renderEnd;

              cachedAttr(enteringEle.parent(), 'nav-view-transition', enteringData.transition);
              cachedAttr(enteringEle.parent(), 'nav-view-direction', enteringData.direction);

              // cancel any previous transition complete fallbacks
              $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER));

              // get the transition ready and see if it'll animate
              var transitionFn = $ionicConfig.transitions.views[enteringData.transition] || $ionicConfig.transitions.views.none;
              var viewTransition = transitionFn(enteringEle, leavingEle, enteringData.direction,
                enteringData.shouldAnimate && allowAnimate && renderEnd);

              if (viewTransition.shouldAnimate) {
                // attach transitionend events (and fallback timer)
                enteringEle.on(TRANSITIONEND_EVENT, completeOnTransitionEnd);
                enteringEle.data(DATA_FALLBACK_TIMER, $timeout(transitionComplete, defaultTimeout));
                $ionicClickBlock.show(defaultTimeout);
              }

              if (renderStart) {
                // notify the views "before" the transition starts
                switcher.emit('before', enteringData, leavingData);

                // stage entering element, opacity 0, no transition duration
                navViewAttr(enteringEle, VIEW_STATUS_STAGED);

                // render the elements in the correct location for their starting point
                viewTransition.run(0);
              }

              if (renderEnd) {
                // create a promise so we can keep track of when all transitions finish
                // only required if this transition should complete
                deferred = $q.defer();
                transitionPromises.push(deferred.promise);
              }

              if (renderStart && renderEnd) {
                // CSS "auto" transitioned, not manually transitioned
                // wait a frame so the styles apply before auto transitioning
                $timeout(function () {
                  ionic.requestAnimationFrame(onReflow);
                });
              } else if (!renderEnd) {
                // just the start of a manual transition
                // but it will not render the end of the transition
                navViewAttr(enteringEle, 'entering');
                navViewAttr(leavingEle, 'leaving');

                // return the transition run method so each step can be ran manually
                return {
                  run: viewTransition.run,
                  cancel: function (shouldAnimate) {
                    if (shouldAnimate) {
                      enteringEle.on(TRANSITIONEND_EVENT, cancelOnTransitionEnd);
                      enteringEle.data(DATA_FALLBACK_TIMER, $timeout(cancelTransition, defaultTimeout));
                      $ionicClickBlock.show(defaultTimeout);
                    } else {
                      cancelTransition();
                    }
                    viewTransition.shouldAnimate = shouldAnimate;
                    viewTransition.run(0);
                    viewTransition = null;
                  }
                };

              } else if (renderEnd) {
                // just the end of a manual transition
                // happens after the manual transition has completed
                // and a full history change has happened
                onReflow();
              }


              function onReflow() {
                // remove that we're staging the entering element so it can auto transition
                navViewAttr(enteringEle, viewTransition.shouldAnimate ? 'entering' : VIEW_STATUS_ACTIVE);
                navViewAttr(leavingEle, viewTransition.shouldAnimate ? 'leaving' : VIEW_STATUS_CACHED);

                // start the auto transition and let the CSS take over
                viewTransition.run(1);

                // trigger auto transitions on the associated nav bars
                $ionicNavBarDelegate._instances.forEach(function (instance) {
                  instance.triggerTransitionStart(transitionId);
                });

                if (!viewTransition.shouldAnimate) {
                  // no animated auto transition
                  transitionComplete();
                }
              }

              // Make sure that transitionend events bubbling up from children won't fire
              // transitionComplete. Will only go forward if ev.target == the element listening.
              function completeOnTransitionEnd(ev) {
                if (ev.target !== this) return;
                transitionComplete();
              }

              function transitionComplete() {
                if (transitionComplete.x) return;
                transitionComplete.x = true;

                enteringEle.off(TRANSITIONEND_EVENT, completeOnTransitionEnd);
                $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER));
                leavingEle && $timeout.cancel(leavingEle.data(DATA_FALLBACK_TIMER));

                // resolve that this one transition (there could be many w/ nested views)
                deferred && deferred.resolve(navViewCtrl);

                // the most recent transition added has completed and all the active
                // transition promises should be added to the services array of promises
                if (transitionId === transitionCounter) {
                  $q.all(transitionPromises).then(ionicViewSwitcher.transitionEnd);

                  // emit that the views have finished transitioning
                  // each parent nav-view will update which views are active and cached
                  switcher.emit('after', enteringData, leavingData);
                  switcher.cleanup(enteringData);
                }

                // tell the nav bars that the transition has ended
                $ionicNavBarDelegate._instances.forEach(function (instance) {
                  instance.triggerTransitionEnd();
                });


                // remove any references that could cause memory issues
                nextTransition = nextDirection = enteringView = leavingView = enteringEle = leavingEle = null;
              }

              // Make sure that transitionend events bubbling up from children won't fire
              // transitionComplete. Will only go forward if ev.target == the element listening.
              function cancelOnTransitionEnd(ev) {
                if (ev.target !== this) return;
                cancelTransition();
              }

              function cancelTransition() {
                navViewAttr(enteringEle, VIEW_STATUS_CACHED);
                navViewAttr(leavingEle, VIEW_STATUS_ACTIVE);
                enteringEle.off(TRANSITIONEND_EVENT, cancelOnTransitionEnd);
                $timeout.cancel(enteringEle.data(DATA_FALLBACK_TIMER));
                ionicViewSwitcher.transitionEnd([navViewCtrl]);
              }

            },

            emit: function (step, enteringData, leavingData) {
              var enteringScope = getScopeForElement(enteringEle, enteringData);
              var leavingScope = getScopeForElement(leavingEle, leavingData);

              var prefixesAreEqual;

              if (!enteringData.viewId || enteringData.abstractView) {
                // it's an abstract view, so treat it accordingly

                // we only get access to the leaving scope once in the transition,
                // so dispatch all events right away if it exists
                if (leavingScope) {
                  leavingScope.$emit('$ionicView.beforeLeave', leavingData);
                  leavingScope.$emit('$ionicView.leave', leavingData);
                  leavingScope.$emit('$ionicView.afterLeave', leavingData);
                  leavingScope.$broadcast('$ionicParentView.beforeLeave', leavingData);
                  leavingScope.$broadcast('$ionicParentView.leave', leavingData);
                  leavingScope.$broadcast('$ionicParentView.afterLeave', leavingData);
                }
              } else {
                // it's a regular view, so do the normal process
                if (step == 'after') {
                  if (enteringScope) {
                    enteringScope.$emit('$ionicView.enter', enteringData);
                    enteringScope.$broadcast('$ionicParentView.enter', enteringData);
                  }

                  if (leavingScope) {
                    leavingScope.$emit('$ionicView.leave', leavingData);
                    leavingScope.$broadcast('$ionicParentView.leave', leavingData);
                  } else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) {
                    // we only want to dispatch this when we are doing a single-tier
                    // state change such as changing a tab, so compare the state
                    // for the same state-prefix but different suffix
                    prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName);
                    if (prefixesAreEqual) {
                      enteringScope.$emit('$ionicNavView.leave', leavingData);
                    }
                  }
                }

                if (enteringScope) {
                  enteringScope.$emit('$ionicView.' + step + 'Enter', enteringData);
                  enteringScope.$broadcast('$ionicParentView.' + step + 'Enter', enteringData);
                }

                if (leavingScope) {
                  leavingScope.$emit('$ionicView.' + step + 'Leave', leavingData);
                  leavingScope.$broadcast('$ionicParentView.' + step + 'Leave', leavingData);

                } else if (enteringScope && leavingData && leavingData.viewId && enteringData.stateName !== leavingData.stateName) {
                  // we only want to dispatch this when we are doing a single-tier
                  // state change such as changing a tab, so compare the state
                  // for the same state-prefix but different suffix
                  prefixesAreEqual = compareStatePrefixes(enteringData.stateName, leavingData.stateName);
                  if (prefixesAreEqual) {
                    enteringScope.$emit('$ionicNavView.' + step + 'Leave', leavingData);
                  }
                }
              }
            },

            cleanup: function (transData) {
              // check if any views should be removed
              if (leavingEle && transData.direction == 'back' && !$ionicConfig.views.forwardCache()) {
                // if they just navigated back we can destroy the forward view
                // do not remove forward views if cacheForwardViews config is true
                destroyViewEle(leavingEle);
              }

              var viewElements = navViewCtrl.getViewElements();
              var viewElementsLength = viewElements.length;
              var x, viewElement;
              var removeOldestAccess = (viewElementsLength - 1) > $ionicConfig.views.maxCache();
              var removableEle;
              var oldestAccess = Date.now();

              for (x = 0; x < viewElementsLength; x++) {
                viewElement = viewElements.eq(x);

                if (removeOldestAccess && viewElement.data(DATA_VIEW_ACCESSED) < oldestAccess) {
                  // remember what was the oldest element to be accessed so it can be destroyed
                  oldestAccess = viewElement.data(DATA_VIEW_ACCESSED);
                  removableEle = viewElements.eq(x);

                } else if (viewElement.data(DATA_DESTROY_ELE) && navViewAttr(viewElement) != VIEW_STATUS_ACTIVE) {
                  destroyViewEle(viewElement);
                }
              }

              destroyViewEle(removableEle);

              if (enteringEle.data(DATA_NO_CACHE)) {
                enteringEle.data(DATA_DESTROY_ELE, true);
              }
            },

            enteringEle: function () {
              return enteringEle;
            },
            leavingEle: function () {
              return leavingEle;
            }

          };

          return switcher;
        },

        transitionEnd: function (navViewCtrls) {
          forEach(navViewCtrls, function (navViewCtrl) {
            navViewCtrl.transitionEnd();
          });

          ionicViewSwitcher.isTransitioning(false);
          $ionicClickBlock.hide();
          transitionPromises = [];
        },

        nextTransition: function (val) {
          nextTransition = val;
        },

        nextDirection: function (val) {
          nextDirection = val;
        },

        isTransitioning: function (val) {
          if (arguments.length) {
            ionic.transition.isActive = !!val;
            $timeout.cancel(isActiveTimer);
            if (val) {
              isActiveTimer = $timeout(function () {
                ionicViewSwitcher.isTransitioning(false);
              }, 999);
            }
          }
          return ionic.transition.isActive;
        },

        createViewEle: function (viewLocals) {
          var containerEle = $document[0].createElement('div');
          if (viewLocals && viewLocals.$template) {
            containerEle.innerHTML = viewLocals.$template;
            if (containerEle.children.length === 1) {
              containerEle.children[0].classList.add('pane');
              if (viewLocals.$$state && viewLocals.$$state.self && viewLocals.$$state.self['abstract']) {
                angular.element(containerEle.children[0]).attr("abstract", "true");
              } else {
                if (viewLocals.$$state && viewLocals.$$state.self) {
                  angular.element(containerEle.children[0]).attr("state", viewLocals.$$state.self.name);
                }

              }
              return jqLite(containerEle.children[0]);
            }
          }
          containerEle.className = "pane";
          return jqLite(containerEle);
        },

        viewEleIsActive: function (viewEle, isActiveAttr) {
          navViewAttr(viewEle, isActiveAttr ? VIEW_STATUS_ACTIVE : VIEW_STATUS_CACHED);
        },

        getTransitionData: getTransitionData,
        navViewAttr: navViewAttr,
        destroyViewEle: destroyViewEle

      };

      return ionicViewSwitcher;


      function getViewElementIdentifier(locals, view) {
        if (viewState(locals)['abstract']) return viewState(locals).name;
        if (view) return view.stateId || view.viewId;
        return ionic.Utils.nextUid();
      }

      function viewState(locals) {
        return locals && locals.$$state && locals.$$state.self || {};
      }

      function getTransitionData(viewLocals, enteringEle, direction, view) {
        // Priority
        // 1) attribute directive on the button/link to this view
        // 2) entering element's attribute
        // 3) entering view's $state config property
        // 4) view registration data
        // 5) global config
        // 6) fallback value

        var state = viewState(viewLocals);
        var viewTransition = nextTransition || cachedAttr(enteringEle, 'view-transition') || state.viewTransition || $ionicConfig.views.transition() || 'ios';
        var navBarTransition = $ionicConfig.navBar.transition();
        direction = nextDirection || cachedAttr(enteringEle, 'view-direction') || state.viewDirection || direction || 'none';

        return extend(getViewData(view), {
          transition: viewTransition,
          navBarTransition: navBarTransition === 'view' ? viewTransition : navBarTransition,
          direction: direction,
          shouldAnimate: (viewTransition !== 'none' && direction !== 'none')
        });
      }

      function getViewData(view) {
        view = view || {};
        return {
          viewId: view.viewId,
          historyId: view.historyId,
          stateId: view.stateId,
          stateName: view.stateName,
          stateParams: view.stateParams
        };
      }

      function navViewAttr(ele, value) {
        if (arguments.length > 1) {
          cachedAttr(ele, NAV_VIEW_ATTR, value);
        } else {
          return cachedAttr(ele, NAV_VIEW_ATTR);
        }
      }

      function destroyViewEle(ele) {
        // we found an element that should be removed
        // destroy its scope, then remove the element
        if (ele && ele.length) {
          var viewScope = ele.scope();
          if (viewScope) {
            viewScope.$emit('$ionicView.unloaded', ele.data(DATA_VIEW));
            viewScope.$destroy();
          }
          ele.remove();
        }
      }

      function compareStatePrefixes(enteringStateName, exitingStateName) {
        var enteringStateSuffixIndex = enteringStateName.lastIndexOf('.');
        var exitingStateSuffixIndex = exitingStateName.lastIndexOf('.');

        // if either of the prefixes are empty, just return false
        if (enteringStateSuffixIndex < 0 || exitingStateSuffixIndex < 0) {
          return false;
        }

        var enteringPrefix = enteringStateName.substring(0, enteringStateSuffixIndex);
        var exitingPrefix = exitingStateName.substring(0, exitingStateSuffixIndex);

        return enteringPrefix === exitingPrefix;
      }

      function getScopeForElement(element, stateData) {
        if (!element) {
          return null;
        }
        // check if it's abstract
        var attributeValue = angular.element(element).attr("abstract");
        var stateValue = angular.element(element).attr("state");

        if (attributeValue !== "true") {
          // it's not an abstract view, so make sure the element
          // matches the state.  Due to abstract view weirdness,
          // sometimes it doesn't. If it doesn't, don't dispatch events
          // so leave the scope undefined
          if (stateValue === stateData.stateName) {
            return angular.element(element).scope();
          }
          return null;
        } else {
          // it is an abstract element, so look for element with the "state" attributeValue
          // set to the name of the stateData state
          var elements = aggregateNavViewChildren(element);
          for (var i = 0; i < elements.length; i++) {
            var state = angular.element(elements[i]).attr("state");
            if (state === stateData.stateName) {
              stateData.abstractView = true;
              return angular.element(elements[i]).scope();
            }
          }
          // we didn't find a match, so return null
          return null;
        }
      }

      function aggregateNavViewChildren(element) {
        var aggregate = [];
        var navViews = angular.element(element).find("ion-nav-view");
        for (var i = 0; i < navViews.length; i++) {
          var children = angular.element(navViews[i]).children();
          var childrenAggregated = [];
          for (var j = 0; j < children.length; j++) {
            childrenAggregated = childrenAggregated.concat(children[j]);
          }
          aggregate = aggregate.concat(childrenAggregated);
        }
        return aggregate;
      }

    }]);

  /**
   * ==================  angular-ios9-uiwebview.patch.js v1.1.1 ==================
   *
   * This patch works around iOS9 UIWebView regression that causes infinite digest
   * errors in Angular.
   *
   * The patch can be applied to Angular 1.2.0 – 1.4.5. Newer versions of Angular
   * have the workaround baked in.
   *
   * To apply this patch load/bundle this file with your application and add a
   * dependency on the "ngIOS9UIWebViewPatch" module to your main app module.
   *
   * For example:
   *
   * ```
   * angular.module('learn-ionic-main', ['ngRoute'])`
   * ```
   *
   * becomes
   *
   * ```
   * angular.module('learn-ionic-main', ['ngRoute', 'ngIOS9UIWebViewPatch'])
   * ```
   *
   *
   * More info:
   * - https://openradar.appspot.com/22186109
   * - https://github.com/angular/angular.js/issues/12241
   * - https://github.com/ionic-team/ionic/issues/4082
   *
   *
   * @license AngularJS
   * (c) 2010-2015 Google, Inc. http://angularjs.org
   * License: MIT
   */

  angular.module('ngIOS9UIWebViewPatch', ['ng']).config(['$provide', function ($provide) {
    'use strict';

    $provide.decorator('$browser', ['$delegate', '$window', function ($delegate, $window) {

      if (isIOS9UIWebView($window.navigator.userAgent)) {
        return applyIOS9Shim($delegate);
      }

      return $delegate;

      function isIOS9UIWebView(userAgent) {
        return /(iPhone|iPad|iPod).* OS 9_\d/.test(userAgent) && !/Version\/9\./.test(userAgent);
      }

      function applyIOS9Shim(browser) {
        var pendingLocationUrl = null;
        var originalUrlFn = browser.url;

        browser.url = function () {
          if (arguments.length) {
            pendingLocationUrl = arguments[0];
            return originalUrlFn.apply(browser, arguments);
          }

          return pendingLocationUrl || originalUrlFn.apply(browser, arguments);
        };

        window.addEventListener('popstate', clearPendingLocationUrl, false);
        window.addEventListener('hashchange', clearPendingLocationUrl, false);

        function clearPendingLocationUrl() {
          pendingLocationUrl = null;
        }

        return browser;
      }
    }]);
  }]);

  /**
   * @private
   * Parts of Ionic requires that $scope data is attached to the element.
   * We do not want to disable adding $scope data to the $element when
   * $compileProvider.debugInfoEnabled(false) is used.
   */
  IonicModule.config(['$provide', function ($provide) {
    $provide.decorator('$compile', ['$delegate', function ($compile) {
      $compile.$$addScopeInfo = function $$addScopeInfo($element, scope, isolated, noTemplate) {
        var dataName = isolated ? (noTemplate ? '$isolateScopeNoTemplate' : '$isolateScope') : '$scope';
        $element.data(dataName, scope);
      };
      return $compile;
    }]);
  }]);

  /**
   * @private
   */
  IonicModule.config([
    '$provide',
    function ($provide) {
      function $LocationDecorator($location, $timeout) {

        $location.__hash = $location.hash;
        //Fix: when window.location.hash is set, the scrollable area
        //found nearest to body's scrollTop is set to scroll to an element
        //with that ID.
        $location.hash = function (value) {
          if (isDefined(value) && value.length > 0) {
            $timeout(function () {
              var scroll = document.querySelector('.scroll-content');
              if (scroll) {
                scroll.scrollTop = 0;
              }
            }, 0, false);
          }
          return $location.__hash(value);
        };

        return $location;
      }

      $provide.decorator('$location', ['$delegate', '$timeout', $LocationDecorator]);
    }]);

  IonicModule

  .controller('$ionicHeaderBar', [
    '$scope',
    '$element',
    '$attrs',
    '$q',
    '$ionicConfig',
    '$ionicHistory',
    function ($scope, $element, $attrs, $q, $ionicConfig, $ionicHistory) {
      var TITLE = 'title';
      var BACK_TEXT = 'back-text';
      var BACK_BUTTON = 'back-button';
      var DEFAULT_TITLE = 'default-title';
      var PREVIOUS_TITLE = 'previous-title';
      var HIDE = 'hide';

      var self = this;
      var titleText = '';
      var previousTitleText = '';
      var titleLeft = 0;
      var titleRight = 0;
      var titleCss = '';
      var isBackEnabled = false;
      var isBackShown = true;
      var isNavBackShown = true;
      var isBackElementShown = false;
      var titleTextWidth = 0;


      self.beforeEnter = function (viewData) {
        $scope.$broadcast('$ionicView.beforeEnter', viewData);
      };


      self.title = function (newTitleText) {
        if (arguments.length && newTitleText !== titleText) {
          getEle(TITLE).innerHTML = newTitleText;
          titleText = newTitleText;
          titleTextWidth = 0;
        }
        return titleText;
      };


      self.enableBack = function (shouldEnable, disableReset) {
        // whether or not the back button show be visible, according
        // to the navigation and history
        if (arguments.length) {
          isBackEnabled = shouldEnable;
          if (!disableReset) self.updateBackButton();
        }
        return isBackEnabled;
      };


      self.showBack = function (shouldShow, disableReset) {
        // different from enableBack() because this will always have the back
        // visually hidden if false, even if the history says it should show
        if (arguments.length) {
          isBackShown = shouldShow;
          if (!disableReset) self.updateBackButton();
        }
        return isBackShown;
      };


      self.showNavBack = function (shouldShow) {
        // different from showBack() because this is for the entire nav bar's
        // setting for all of it's child headers. For internal use.
        isNavBackShown = shouldShow;
        self.updateBackButton();
      };


      self.updateBackButton = function () {
        var ele;
        if ((isBackShown && isNavBackShown && isBackEnabled) !== isBackElementShown) {
          isBackElementShown = isBackShown && isNavBackShown && isBackEnabled;
          ele = getEle(BACK_BUTTON);
          ele && ele.classList[isBackElementShown ? 'remove' : 'add'](HIDE);
        }

        if (isBackEnabled) {
          ele = ele || getEle(BACK_BUTTON);
          if (ele) {
            if (self.backButtonIcon !== $ionicConfig.backButton.icon()) {
              ele = getEle(BACK_BUTTON + ' .icon');
              if (ele) {
                self.backButtonIcon = $ionicConfig.backButton.icon();
                ele.className = 'icon ' + self.backButtonIcon;
              }
            }

            if (self.backButtonText !== $ionicConfig.backButton.text()) {
              ele = getEle(BACK_BUTTON + ' .back-text');
              if (ele) {
                ele.textContent = self.backButtonText = $ionicConfig.backButton.text();
              }
            }
          }
        }
      };


      self.titleTextWidth = function () {
        var element = getEle(TITLE);
        if (element) {
          // If the element has a nav-bar-title, use that instead
          // to calculate the width of the title
          var children = angular.element(element).children();
          for (var i = 0; i < children.length; i++) {
            if (angular.element(children[i]).hasClass('nav-bar-title')) {
              element = children[i];
              break;
            }
          }
        }
        var bounds = ionic.DomUtil.getTextBounds(element);
        titleTextWidth = Math.min(bounds && bounds.width || 30);
        return titleTextWidth;
      };


      self.titleWidth = function () {
        var titleWidth = self.titleTextWidth();
        var offsetWidth = getEle(TITLE).offsetWidth;
        if (offsetWidth < titleWidth) {
          titleWidth = offsetWidth + (titleLeft - titleRight - 5);
        }
        return titleWidth;
      };


      self.titleTextX = function () {
        return ($element[0].offsetWidth / 2) - (self.titleWidth() / 2);
      };


      self.titleLeftRight = function () {
        return titleLeft - titleRight;
      };


      self.backButtonTextLeft = function () {
        var offsetLeft = 0;
        var ele = getEle(BACK_TEXT);
        while (ele) {
          offsetLeft += ele.offsetLeft;
          ele = ele.parentElement;
        }
        return offsetLeft;
      };


      self.resetBackButton = function (viewData) {
        if ($ionicConfig.backButton.previousTitleText()) {
          var previousTitleEle = getEle(PREVIOUS_TITLE);
          if (previousTitleEle) {
            previousTitleEle.classList.remove(HIDE);

            var view = (viewData && $ionicHistory.getViewById(viewData.viewId));
            var newPreviousTitleText = $ionicHistory.backTitle(view);

            if (newPreviousTitleText !== previousTitleText) {
              previousTitleText = previousTitleEle.innerHTML = newPreviousTitleText;
            }
          }
          var defaultTitleEle = getEle(DEFAULT_TITLE);
          if (defaultTitleEle) {
            defaultTitleEle.classList.remove(HIDE);
          }
        }
      };


      self.align = function (textAlign) {
        var titleEle = getEle(TITLE);

        textAlign = textAlign || $attrs.alignTitle || $ionicConfig.navBar.alignTitle();

        var widths = self.calcWidths(textAlign, false);

        if (isBackShown && previousTitleText && $ionicConfig.backButton.previousTitleText()) {
          var previousTitleWidths = self.calcWidths(textAlign, true);

          var availableTitleWidth = $element[0].offsetWidth - previousTitleWidths.titleLeft - previousTitleWidths.titleRight;

          if (self.titleTextWidth() <= availableTitleWidth) {
            widths = previousTitleWidths;
          }
        }

        return self.updatePositions(titleEle, widths.titleLeft, widths.titleRight, widths.buttonsLeft, widths.buttonsRight, widths.css, widths.showPrevTitle);
      };


      self.calcWidths = function (textAlign, isPreviousTitle) {
        var titleEle = getEle(TITLE);
        var backBtnEle = getEle(BACK_BUTTON);
        var x, y, z, b, c, d, childSize, bounds;
        var childNodes = $element[0].childNodes;
        var buttonsLeft = 0;
        var buttonsRight = 0;
        var isCountRightOfTitle;
        var updateTitleLeft = 0;
        var updateTitleRight = 0;
        var updateCss = '';
        var backButtonWidth = 0;

        // Compute how wide the left children are
        // Skip all titles (there may still be two titles, one leaving the dom)
        // Once we encounter a titleEle, realize we are now counting the right-buttons, not left
        for (x = 0; x < childNodes.length; x++) {
          c = childNodes[x];

          childSize = 0;
          if (c.nodeType == 1) {
            // element node
            if (c === titleEle) {
              isCountRightOfTitle = true;
              continue;
            }

            if (c.classList.contains(HIDE)) {
              continue;
            }

            if (isBackShown && c === backBtnEle) {

              for (y = 0; y < c.childNodes.length; y++) {
                b = c.childNodes[y];

                if (b.nodeType == 1) {

                  if (b.classList.contains(BACK_TEXT)) {
                    for (z = 0; z < b.children.length; z++) {
                      d = b.children[z];

                      if (isPreviousTitle) {
                        if (d.classList.contains(DEFAULT_TITLE)) continue;
                        backButtonWidth += d.offsetWidth;
                      } else {
                        if (d.classList.contains(PREVIOUS_TITLE)) continue;
                        backButtonWidth += d.offsetWidth;
                      }
                    }

                  } else {
                    backButtonWidth += b.offsetWidth;
                  }

                } else if (b.nodeType == 3 && b.nodeValue.trim()) {
                  bounds = ionic.DomUtil.getTextBounds(b);
                  backButtonWidth += bounds && bounds.width || 0;
                }

              }
              childSize = backButtonWidth || c.offsetWidth;

            } else {
              // not the title, not the back button, not a hidden element
              childSize = c.offsetWidth;
            }

          } else if (c.nodeType == 3 && c.nodeValue.trim()) {
            // text node
            bounds = ionic.DomUtil.getTextBounds(c);
            childSize = bounds && bounds.width || 0;
          }

          if (isCountRightOfTitle) {
            buttonsRight += childSize;
          } else {
            buttonsLeft += childSize;
          }
        }

        // Size and align the header titleEle based on the sizes of the left and
        // right children, and the desired alignment mode
        if (textAlign == 'left') {
          updateCss = 'title-left';
          if (buttonsLeft) {
            updateTitleLeft = buttonsLeft + 15;
          }
          if (buttonsRight) {
            updateTitleRight = buttonsRight + 15;
          }

        } else if (textAlign == 'right') {
          updateCss = 'title-right';
          if (buttonsLeft) {
            updateTitleLeft = buttonsLeft + 15;
          }
          if (buttonsRight) {
            updateTitleRight = buttonsRight + 15;
          }

        } else {
          // center the default
          var margin = Math.max(buttonsLeft, buttonsRight) + 10;
          if (margin > 10) {
            updateTitleLeft = updateTitleRight = margin;
          }
        }

        return {
          backButtonWidth: backButtonWidth,
          buttonsLeft: buttonsLeft,
          buttonsRight: buttonsRight,
          titleLeft: updateTitleLeft,
          titleRight: updateTitleRight,
          showPrevTitle: isPreviousTitle,
          css: updateCss
        };
      };


      self.updatePositions = function (titleEle, updateTitleLeft, updateTitleRight, buttonsLeft, buttonsRight, updateCss, showPreviousTitle) {
        var deferred = $q.defer();

        // only make DOM updates when there are actual changes
        if (titleEle) {
          if (updateTitleLeft !== titleLeft) {
            titleEle.style.left = updateTitleLeft ? updateTitleLeft + 'px' : '';
            titleLeft = updateTitleLeft;
          }
          if (updateTitleRight !== titleRight) {
            titleEle.style.right = updateTitleRight ? updateTitleRight + 'px' : '';
            titleRight = updateTitleRight;
          }

          if (updateCss !== titleCss) {
            updateCss && titleEle.classList.add(updateCss);
            titleCss && titleEle.classList.remove(titleCss);
            titleCss = updateCss;
          }
        }

        if ($ionicConfig.backButton.previousTitleText()) {
          var prevTitle = getEle(PREVIOUS_TITLE);
          var defaultTitle = getEle(DEFAULT_TITLE);

          prevTitle && prevTitle.classList[showPreviousTitle ? 'remove' : 'add'](HIDE);
          defaultTitle && defaultTitle.classList[showPreviousTitle ? 'add' : 'remove'](HIDE);
        }

        ionic.requestAnimationFrame(function () {
          if (titleEle && titleEle.offsetWidth + 10 < titleEle.scrollWidth) {
            var minRight = buttonsRight + 5;
            var testRight = $element[0].offsetWidth - titleLeft - self.titleTextWidth() - 20;
            updateTitleRight = testRight < minRight ? minRight : testRight;
            if (updateTitleRight !== titleRight) {
              titleEle.style.right = updateTitleRight + 'px';
              titleRight = updateTitleRight;
            }
          }
          deferred.resolve();
        });

        return deferred.promise;
      };


      self.setCss = function (elementClassname, css) {
        ionic.DomUtil.cachedStyles(getEle(elementClassname), css);
      };


      var eleCache = {};

      function getEle(className) {
        if (!eleCache[className]) {
          eleCache[className] = $element[0].querySelector('.' + className);
        }
        return eleCache[className];
      }


      $scope.$on('$destroy', function () {
        for (var n in eleCache) eleCache[n] = null;
      });

    }]);

  IonicModule
  .controller('$ionInfiniteScroll', [
    '$scope',
    '$attrs',
    '$element',
    '$timeout',
    function ($scope, $attrs, $element, $timeout) {
      var self = this;
      self.isLoading = false;

      $scope.icon = function () {
        return isDefined($attrs.icon) ? $attrs.icon : 'ion-load-d';
      };

      $scope.spinner = function () {
        return isDefined($attrs.spinner) ? $attrs.spinner : '';
      };

      $scope.$on('scroll.infiniteScrollComplete', function () {
        finishInfiniteScroll();
      });

      $scope.$on('$destroy', function () {
        if (self.scrollCtrl && self.scrollCtrl.$element) self.scrollCtrl.$element.off('scroll', self.checkBounds);
        if (self.scrollEl && self.scrollEl.removeEventListener) {
          self.scrollEl.removeEventListener('scroll', self.checkBounds);
        }
      });

      // debounce checking infinite scroll events
      self.checkBounds = ionic.Utils.throttle(checkInfiniteBounds, 300);

      function onInfinite() {
        ionic.requestAnimationFrame(function () {
          $element[0].classList.add('active');
        });
        self.isLoading = true;
        $scope.$parent && $scope.$parent.$apply($attrs.onInfinite || '');
      }

      function finishInfiniteScroll() {
        ionic.requestAnimationFrame(function () {
          $element[0].classList.remove('active');
        });
        $timeout(function () {
          if (self.jsScrolling) self.scrollView.resize();
          // only check bounds again immediately if the page isn't cached (scroll el has height)
          if ((self.jsScrolling && self.scrollView.__container && self.scrollView.__container.offsetHeight > 0) ||
            !self.jsScrolling) {
            self.checkBounds();
          }
        }, 30, false);
        self.isLoading = false;
      }

      // check if we've scrolled far enough to trigger an infinite scroll
      function checkInfiniteBounds() {
        if (self.isLoading) return;
        var maxScroll = {};

        if (self.jsScrolling) {
          maxScroll = self.getJSMaxScroll();
          var scrollValues = self.scrollView.getValues();
          if ((maxScroll.left !== -1 && scrollValues.left >= maxScroll.left) ||
            (maxScroll.top !== -1 && scrollValues.top >= maxScroll.top)) {
            onInfinite();
          }
        } else {
          maxScroll = self.getNativeMaxScroll();
          if ((
            maxScroll.left !== -1 &&
            self.scrollEl.scrollLeft >= maxScroll.left - self.scrollEl.clientWidth
          ) || (
            maxScroll.top !== -1 &&
            self.scrollEl.scrollTop >= maxScroll.top - self.scrollEl.clientHeight
          )) {
            onInfinite();
          }
        }
      }

      // determine the threshold at which we should fire an infinite scroll
      // note: this gets processed every scroll event, can it be cached?
      self.getJSMaxScroll = function () {
        var maxValues = self.scrollView.getScrollMax();
        return {
          left: self.scrollView.options.scrollingX ?
            calculateMaxValue(maxValues.left) :
            -1,
          top: self.scrollView.options.scrollingY ?
            calculateMaxValue(maxValues.top) :
            -1
        };
      };

      self.getNativeMaxScroll = function () {
        var maxValues = {
          left: self.scrollEl.scrollWidth,
          top: self.scrollEl.scrollHeight
        };
        var computedStyle = window.getComputedStyle(self.scrollEl) || {};
        return {
          left: maxValues.left &&
          (computedStyle.overflowX === 'scroll' ||
            computedStyle.overflowX === 'auto' ||
            self.scrollEl.style['overflow-x'] === 'scroll') ?
            calculateMaxValue(maxValues.left) : -1,
          top: maxValues.top &&
          (computedStyle.overflowY === 'scroll' ||
            computedStyle.overflowY === 'auto' ||
            self.scrollEl.style['overflow-y'] === 'scroll') ?
            calculateMaxValue(maxValues.top) : -1
        };
      };

      // determine pixel refresh distance based on % or value
      function calculateMaxValue(maximum) {
        var distance = ($attrs.distance || '2.5%').trim();
        var isPercent = distance.indexOf('%') !== -1;
        return isPercent ?
          maximum * (1 - parseFloat(distance) / 100) :
          maximum - parseFloat(distance);
      }

      //for testing
      self.__finishInfiniteScroll = finishInfiniteScroll;

    }]);

  /**
   * @ngdoc service
   * @name $ionicListDelegate
   * @module ionic
   *
   * @description
   * Delegate for controlling the {@link ionic.directive:ionList} directive.
   *
   * Methods called directly on the $ionicListDelegate service will control all lists.
   * Use the {@link ionic.service:$ionicListDelegate#$getByHandle $getByHandle}
   * method to control specific ionList instances.
   *
   * @usage
   * ```html
   * {% raw %}
   * <ion-content ng-controller="MyCtrl">
   *   <button class="button" ng-click="showDeleteButtons()"></button>
   *   <ion-list>
   *     <ion-item ng-repeat="i in items">
   *       Hello, {{i}}!
   *       <ion-delete-button class="ion-minus-circled"></ion-delete-button>
   *     </ion-item>
   *   </ion-list>
   * </ion-content>
   * {% endraw %}
   * ```

   * ```js
   * function MyCtrl($scope, $ionicListDelegate) {
   *   $scope.showDeleteButtons = function() {
   *     $ionicListDelegate.showDelete(true);
   *   };
   * }
   * ```
   */
  IonicModule.service('$ionicListDelegate', ionic.DelegateService([
    /**
     * @ngdoc method
     * @name $ionicListDelegate#showReorder
     * @param {boolean=} showReorder Set whether or not this list is showing its reorder buttons.
     * @returns {boolean} Whether the reorder buttons are shown.
     */
    'showReorder',
    /**
     * @ngdoc method
     * @name $ionicListDelegate#showDelete
     * @param {boolean=} showDelete Set whether or not this list is showing its delete buttons.
     * @returns {boolean} Whether the delete buttons are shown.
     */
    'showDelete',
    /**
     * @ngdoc method
     * @name $ionicListDelegate#canSwipeItems
     * @param {boolean=} canSwipeItems Set whether or not this list is able to swipe to show
     * option buttons.
     * @returns {boolean} Whether the list is able to swipe to show option buttons.
     */
    'canSwipeItems',
    /**
     * @ngdoc method
     * @name $ionicListDelegate#closeOptionButtons
     * @description Closes any option buttons on the list that are swiped open.
     */
    'closeOptionButtons'
    /**
     * @ngdoc method
     * @name $ionicListDelegate#$getByHandle
     * @param {string} handle
     * @returns `delegateInstance` A delegate instance that controls only the
     * {@link ionic.directive:ionList} directives with `delegate-handle` matching
     * the given handle.
     *
     * Example: `$ionicListDelegate.$getByHandle('my-handle').showReorder(true);`
     */
  ]))

  .controller('$ionicList', [
    '$scope',
    '$attrs',
    '$ionicListDelegate',
    '$ionicHistory',
    function ($scope, $attrs, $ionicListDelegate, $ionicHistory) {
      var self = this;
      var isSwipeable = true;
      var isReorderShown = false;
      var isDeleteShown = false;

      var deregisterInstance = $ionicListDelegate._registerInstance(
        self, $attrs.delegateHandle, function () {
          return $ionicHistory.isActiveScope($scope);
        }
      );
      $scope.$on('$destroy', deregisterInstance);

      self.showReorder = function (show) {
        if (arguments.length) {
          isReorderShown = !!show;
        }
        return isReorderShown;
      };

      self.showDelete = function (show) {
        if (arguments.length) {
          isDeleteShown = !!show;
        }
        return isDeleteShown;
      };

      self.canSwipeItems = function (can) {
        if (arguments.length) {
          isSwipeable = !!can;
        }
        return isSwipeable;
      };

      self.closeOptionButtons = function () {
        self.listView && self.listView.clearDragEffects();
      };
    }]);

  IonicModule

  .controller('$ionicNavBar', [
    '$scope',
    '$element',
    '$attrs',
    '$compile',
    '$timeout',
    '$ionicNavBarDelegate',
    '$ionicConfig',
    '$ionicHistory',
    function ($scope, $element, $attrs, $compile, $timeout, $ionicNavBarDelegate, $ionicConfig, $ionicHistory) {

      var CSS_HIDE = 'hide';
      var DATA_NAV_BAR_CTRL = '$ionNavBarController';
      var PRIMARY_BUTTONS = 'primaryButtons';
      var SECONDARY_BUTTONS = 'secondaryButtons';
      var BACK_BUTTON = 'backButton';
      var ITEM_TYPES = 'primaryButtons secondaryButtons leftButtons rightButtons title'.split(' ');

      var self = this;
      var headerBars = [];
      var navElementHtml = {};
      var isVisible = true;
      var queuedTransitionStart, queuedTransitionEnd, latestTransitionId;

      $element.parent().data(DATA_NAV_BAR_CTRL, self);

      var delegateHandle = $attrs.delegateHandle || 'navBar' + ionic.Utils.nextUid();

      var deregisterInstance = $ionicNavBarDelegate._registerInstance(self, delegateHandle);


      self.init = function () {
        $element.addClass('nav-bar-container');
        ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', $ionicConfig.views.transition());

        // create two nav bar blocks which will trade out which one is shown
        self.createHeaderBar(false);
        self.createHeaderBar(true);

        $scope.$emit('ionNavBar.init', delegateHandle);
      };


      self.createHeaderBar = function (isActive) {
        var containerEle = jqLite('<div class="nav-bar-block">');
        ionic.DomUtil.cachedAttr(containerEle, 'nav-bar', isActive ? 'active' : 'cached');

        var alignTitle = $attrs.alignTitle || $ionicConfig.navBar.alignTitle();
        var headerBarEle = jqLite('<ion-header-bar>').addClass($attrs['class']).attr('align-title', alignTitle);
        if (isDefined($attrs.noTapScroll)) headerBarEle.attr('no-tap-scroll', $attrs.noTapScroll);
        var titleEle = jqLite('<div class="title title-' + alignTitle + '">');
        var navEle = {};
        var lastViewItemEle = {};
        var leftButtonsEle, rightButtonsEle;

        navEle[BACK_BUTTON] = createNavElement(BACK_BUTTON);
        navEle[BACK_BUTTON] && headerBarEle.append(navEle[BACK_BUTTON]);

        // append title in the header, this is the rock to where buttons append
        headerBarEle.append(titleEle);

        forEach(ITEM_TYPES, function (itemType) {
          // create default button elements
          navEle[itemType] = createNavElement(itemType);
          // append and position buttons
          positionItem(navEle[itemType], itemType);
        });

        // add header-item to the root children
        for (var x = 0; x < headerBarEle[0].children.length; x++) {
          headerBarEle[0].children[x].classList.add('header-item');
        }

        // compile header and append to the DOM
        containerEle.append(headerBarEle);
        $element.append($compile(containerEle)($scope.$new()));

        var headerBarCtrl = headerBarEle.data('$ionHeaderBarController');
        headerBarCtrl.backButtonIcon = $ionicConfig.backButton.icon();
        headerBarCtrl.backButtonText = $ionicConfig.backButton.text();

        var headerBarInstance = {
          isActive: isActive,
          title: function (newTitleText) {
            headerBarCtrl.title(newTitleText);
          },
          setItem: function (navBarItemEle, itemType) {
            // first make sure any exiting nav bar item has been removed
            headerBarInstance.removeItem(itemType);

            if (navBarItemEle) {
              if (itemType === 'title') {
                // clear out the text based title
                headerBarInstance.title("");
              }

              // there's a custom nav bar item
              positionItem(navBarItemEle, itemType);

              if (navEle[itemType]) {
                // make sure the default on this itemType is hidden
                navEle[itemType].addClass(CSS_HIDE);
              }
              lastViewItemEle[itemType] = navBarItemEle;

            } else if (navEle[itemType]) {
              // there's a default button for this side and no view button
              navEle[itemType].removeClass(CSS_HIDE);
            }
          },
          removeItem: function (itemType) {
            if (lastViewItemEle[itemType]) {
              lastViewItemEle[itemType].scope().$destroy();
              lastViewItemEle[itemType].remove();
              lastViewItemEle[itemType] = null;
            }
          },
          containerEle: function () {
            return containerEle;
          },
          headerBarEle: function () {
            return headerBarEle;
          },
          afterLeave: function () {
            forEach(ITEM_TYPES, function (itemType) {
              headerBarInstance.removeItem(itemType);
            });
            headerBarCtrl.resetBackButton();
          },
          controller: function () {
            return headerBarCtrl;
          },
          destroy: function () {
            forEach(ITEM_TYPES, function (itemType) {
              headerBarInstance.removeItem(itemType);
            });
            containerEle.scope().$destroy();
            for (var n in navEle) {
              if (navEle[n]) {
                navEle[n].removeData();
                navEle[n] = null;
              }
            }
            leftButtonsEle && leftButtonsEle.removeData();
            rightButtonsEle && rightButtonsEle.removeData();
            titleEle.removeData();
            headerBarEle.removeData();
            containerEle.remove();
            containerEle = headerBarEle = titleEle = leftButtonsEle = rightButtonsEle = null;
          }
        };

        function positionItem(ele, itemType) {
          if (!ele) return;

          if (itemType === 'title') {
            // title element
            titleEle.append(ele);

          } else if (itemType == 'rightButtons' ||
            (itemType == SECONDARY_BUTTONS && $ionicConfig.navBar.positionSecondaryButtons() != 'left') ||
            (itemType == PRIMARY_BUTTONS && $ionicConfig.navBar.positionPrimaryButtons() == 'right')) {
            // right side
            if (!rightButtonsEle) {
              rightButtonsEle = jqLite('<div class="buttons buttons-right">');
              headerBarEle.append(rightButtonsEle);
            }
            if (itemType == SECONDARY_BUTTONS) {
              rightButtonsEle.append(ele);
            } else {
              rightButtonsEle.prepend(ele);
            }

          } else {
            // left side
            if (!leftButtonsEle) {
              leftButtonsEle = jqLite('<div class="buttons buttons-left">');
              if (navEle[BACK_BUTTON]) {
                navEle[BACK_BUTTON].after(leftButtonsEle);
              } else {
                headerBarEle.prepend(leftButtonsEle);
              }
            }
            if (itemType == SECONDARY_BUTTONS) {
              leftButtonsEle.append(ele);
            } else {
              leftButtonsEle.prepend(ele);
            }
          }

        }

        headerBars.push(headerBarInstance);

        return headerBarInstance;
      };


      self.navElement = function (type, html) {
        if (isDefined(html)) {
          navElementHtml[type] = html;
        }
        return navElementHtml[type];
      };


      self.update = function (viewData) {
        var showNavBar = !viewData.hasHeaderBar && viewData.showNavBar;
        viewData.transition = $ionicConfig.views.transition();

        if (!showNavBar) {
          viewData.direction = 'none';
        }

        self.enable(showNavBar);
        var enteringHeaderBar = self.isInitialized ? getOffScreenHeaderBar() : getOnScreenHeaderBar();
        var leavingHeaderBar = self.isInitialized ? getOnScreenHeaderBar() : null;
        var enteringHeaderCtrl = enteringHeaderBar.controller();

        // update if the entering header should show the back button or not
        enteringHeaderCtrl.enableBack(viewData.enableBack, true);
        enteringHeaderCtrl.showBack(viewData.showBack, true);
        enteringHeaderCtrl.updateBackButton();

        // update the entering header bar's title
        self.title(viewData.title, enteringHeaderBar);

        self.showBar(showNavBar);

        // update the nav bar items, depending if the view has their own or not
        if (viewData.navBarItems) {
          forEach(ITEM_TYPES, function (itemType) {
            enteringHeaderBar.setItem(viewData.navBarItems[itemType], itemType);
          });
        }

        // begin transition of entering and leaving header bars
        self.transition(enteringHeaderBar, leavingHeaderBar, viewData);

        self.isInitialized = true;
        navSwipeAttr('');
      };


      self.transition = function (enteringHeaderBar, leavingHeaderBar, viewData) {
        var enteringHeaderBarCtrl = enteringHeaderBar.controller();
        var transitionFn = $ionicConfig.transitions.navBar[viewData.navBarTransition] || $ionicConfig.transitions.navBar.none;
        var transitionId = viewData.transitionId;

        enteringHeaderBarCtrl.beforeEnter(viewData);

        var navBarTransition = transitionFn(enteringHeaderBar, leavingHeaderBar, viewData.direction, viewData.shouldAnimate && self.isInitialized);

        ionic.DomUtil.cachedAttr($element, 'nav-bar-transition', viewData.navBarTransition);
        ionic.DomUtil.cachedAttr($element, 'nav-bar-direction', viewData.direction);

        if (navBarTransition.shouldAnimate && viewData.renderEnd) {
          navBarAttr(enteringHeaderBar, 'stage');
        } else {
          navBarAttr(enteringHeaderBar, 'entering');
          navBarAttr(leavingHeaderBar, 'leaving');
        }

        enteringHeaderBarCtrl.resetBackButton(viewData);

        navBarTransition.run(0);

        self.activeTransition = {
          run: function (step) {
            navBarTransition.shouldAnimate = false;
            navBarTransition.direction = 'back';
            navBarTransition.run(step);
          },
          cancel: function (shouldAnimate, speed, cancelData) {
            navSwipeAttr(speed);
            navBarAttr(leavingHeaderBar, 'active');
            navBarAttr(enteringHeaderBar, 'cached');
            navBarTransition.shouldAnimate = shouldAnimate;
            navBarTransition.run(0);
            self.activeTransition = navBarTransition = null;

            var runApply;
            if (cancelData.showBar !== self.showBar()) {
              self.showBar(cancelData.showBar);
            }
            if (cancelData.showBackButton !== self.showBackButton()) {
              self.showBackButton(cancelData.showBackButton);
            }
            if (runApply) {
              $scope.$apply();
            }
          },
          complete: function (shouldAnimate, speed) {
            navSwipeAttr(speed);
            navBarTransition.shouldAnimate = shouldAnimate;
            navBarTransition.run(1);
            queuedTransitionEnd = transitionEnd;
          }
        };

        $timeout(enteringHeaderBarCtrl.align, 16);

        queuedTransitionStart = function () {
          if (latestTransitionId !== transitionId) return;

          navBarAttr(enteringHeaderBar, 'entering');
          navBarAttr(leavingHeaderBar, 'leaving');

          navBarTransition.run(1);

          queuedTransitionEnd = function () {
            if (latestTransitionId == transitionId || !navBarTransition.shouldAnimate) {
              transitionEnd();
            }
          };

          queuedTransitionStart = null;
        };

        function transitionEnd() {
          for (var x = 0; x < headerBars.length; x++) {
            headerBars[x].isActive = false;
          }
          enteringHeaderBar.isActive = true;

          navBarAttr(enteringHeaderBar, 'active');
          navBarAttr(leavingHeaderBar, 'cached');

          self.activeTransition = navBarTransition = queuedTransitionEnd = null;
        }

        queuedTransitionStart();
      };


      self.triggerTransitionStart = function (triggerTransitionId) {
        latestTransitionId = triggerTransitionId;
        queuedTransitionStart && queuedTransitionStart();
      };


      self.triggerTransitionEnd = function () {
        queuedTransitionEnd && queuedTransitionEnd();
      };


      self.showBar = function (shouldShow) {
        if (arguments.length) {
          self.visibleBar(shouldShow);
          $scope.$parent.$hasHeader = !!shouldShow;
        }
        return !!$scope.$parent.$hasHeader;
      };


      self.visibleBar = function (shouldShow) {
        if (shouldShow && !isVisible) {
          $element.removeClass(CSS_HIDE);
          self.align();
        } else if (!shouldShow && isVisible) {
          $element.addClass(CSS_HIDE);
        }
        isVisible = shouldShow;
      };


      self.enable = function (val) {
        // set primary to show first
        self.visibleBar(val);

        // set non primary to hide second
        for (var x = 0; x < $ionicNavBarDelegate._instances.length; x++) {
          if ($ionicNavBarDelegate._instances[x] !== self) $ionicNavBarDelegate._instances[x].visibleBar(false);
        }
      };


      /**
       * @ngdoc method
       * @name $ionicNavBar#showBackButton
       * @description Show/hide the nav bar back button when there is a
       * back view. If the back button is not possible, for example, the
       * first view in the stack, then this will not force the back button
       * to show.
       */
      self.showBackButton = function (shouldShow) {
        if (arguments.length) {
          for (var x = 0; x < headerBars.length; x++) {
            headerBars[x].controller().showNavBack(!!shouldShow);
          }
          $scope.$isBackButtonShown = !!shouldShow;
        }
        return $scope.$isBackButtonShown;
      };


      /**
       * @ngdoc method
       * @name $ionicNavBar#showActiveBackButton
       * @description Show/hide only the active header bar's back button.
       */
      self.showActiveBackButton = function (shouldShow) {
        var headerBar = getOnScreenHeaderBar();
        if (headerBar) {
          if (arguments.length) {
            return headerBar.controller().showBack(shouldShow);
          }
          return headerBar.controller().showBack();
        }
      };


      self.title = function (newTitleText, headerBar) {
        if (isDefined(newTitleText)) {
          newTitleText = newTitleText || '';
          headerBar = headerBar || getOnScreenHeaderBar();
          headerBar && headerBar.title(newTitleText);
          $scope.$title = newTitleText;
          $ionicHistory.currentTitle(newTitleText);
        }
        return $scope.$title;
      };


      self.align = function (val, headerBar) {
        headerBar = headerBar || getOnScreenHeaderBar();
        headerBar && headerBar.controller().align(val);
      };


      self.hasTabsTop = function (isTabsTop) {
        $element[isTabsTop ? 'addClass' : 'removeClass']('nav-bar-tabs-top');
      };

      self.hasBarSubheader = function (isBarSubheader) {
        $element[isBarSubheader ? 'addClass' : 'removeClass']('nav-bar-has-subheader');
      };

      // DEPRECATED, as of v1.0.0-beta14 -------
      self.changeTitle = function (val) {
        deprecatedWarning('changeTitle(val)', 'title(val)');
        self.title(val);
      };
      self.setTitle = function (val) {
        deprecatedWarning('setTitle(val)', 'title(val)');
        self.title(val);
      };
      self.getTitle = function () {
        deprecatedWarning('getTitle()', 'title()');
        return self.title();
      };
      self.back = function () {
        deprecatedWarning('back()', '$ionicHistory.goBack()');
        $ionicHistory.goBack();
      };
      self.getPreviousTitle = function () {
        deprecatedWarning('getPreviousTitle()', '$ionicHistory.backTitle()');
        $ionicHistory.goBack();
      };

      function deprecatedWarning(oldMethod, newMethod) {
        var warn = console.warn || console.log;
        warn && warn.call(console, 'navBarController.' + oldMethod + ' is deprecated, please use ' + newMethod + ' instead');
      }

      // END DEPRECATED -------


      function createNavElement(type) {
        if (navElementHtml[type]) {
          return jqLite(navElementHtml[type]);
        }
      }


      function getOnScreenHeaderBar() {
        for (var x = 0; x < headerBars.length; x++) {
          if (headerBars[x].isActive) return headerBars[x];
        }
      }


      function getOffScreenHeaderBar() {
        for (var x = 0; x < headerBars.length; x++) {
          if (!headerBars[x].isActive) return headerBars[x];
        }
      }


      function navBarAttr(ctrl, val) {
        ctrl && ionic.DomUtil.cachedAttr(ctrl.containerEle(), 'nav-bar', val);
      }

      function navSwipeAttr(val) {
        ionic.DomUtil.cachedAttr($element, 'nav-swipe', val);
      }


      $scope.$on('$destroy', function () {
        $scope.$parent.$hasHeader = false;
        $element.parent().removeData(DATA_NAV_BAR_CTRL);
        for (var x = 0; x < headerBars.length; x++) {
          headerBars[x].destroy();
        }
        $element.remove();
        $element = headerBars = null;
        deregisterInstance();
      });

    }]);

  IonicModule
  .controller('$ionicNavView', [
    '$scope',
    '$element',
    '$attrs',
    '$compile',
    '$controller',
    '$ionicNavBarDelegate',
    '$ionicNavViewDelegate',
    '$ionicHistory',
    '$ionicViewSwitcher',
    '$ionicConfig',
    '$ionicScrollDelegate',
    '$ionicSideMenuDelegate',
    function ($scope, $element, $attrs, $compile, $controller, $ionicNavBarDelegate, $ionicNavViewDelegate, $ionicHistory, $ionicViewSwitcher, $ionicConfig, $ionicScrollDelegate, $ionicSideMenuDelegate) {

      var DATA_ELE_IDENTIFIER = '$eleId';
      var DATA_DESTROY_ELE = '$destroyEle';
      var DATA_NO_CACHE = '$noCache';
      var VIEW_STATUS_ACTIVE = 'active';
      var VIEW_STATUS_CACHED = 'cached';

      var self = this;
      var direction;
      var isPrimary = false;
      var navBarDelegate;
      var activeEleId;
      var navViewAttr = $ionicViewSwitcher.navViewAttr;
      var disableRenderStartViewId, disableAnimation;

      self.scope = $scope;
      self.element = $element;

      self.init = function () {
        var navViewName = $attrs.name || '';

        // Find the details of the parent view directive (if any) and use it
        // to derive our own qualified view name, then hang our own details
        // off the DOM so child directives can find it.
        var parent = $element.parent().inheritedData('$uiView');
        var parentViewName = ((parent && parent.state) ? parent.state.name : '');
        if (navViewName.indexOf('@') < 0) navViewName = navViewName + '@' + parentViewName;

        var viewData = {name: navViewName, state: null};
        $element.data('$uiView', viewData);

        var deregisterInstance = $ionicNavViewDelegate._registerInstance(self, $attrs.delegateHandle);
        $scope.$on('$destroy', function () {
          deregisterInstance();

          // ensure no scrolls have been left frozen
          if (self.isSwipeFreeze) {
            $ionicScrollDelegate.freezeAllScrolls(false);
          }
        });

        $scope.$on('$ionicHistory.deselect', self.cacheCleanup);
        $scope.$on('$ionicTabs.top', onTabsTop);
        $scope.$on('$ionicSubheader', onBarSubheader);

        $scope.$on('$ionicTabs.beforeLeave', onTabsLeave);
        $scope.$on('$ionicTabs.afterLeave', onTabsLeave);
        $scope.$on('$ionicTabs.leave', onTabsLeave);

        ionic.Platform.ready(function () {
          if (ionic.Platform.isWebView() && ionic.Platform.isIOS()) {
            self.initSwipeBack();
          }
        });

        return viewData;
      };


      self.register = function (viewLocals) {
        var leavingView = extend({}, $ionicHistory.currentView());

        // register that a view is coming in and get info on how it should transition
        var registerData = $ionicHistory.register($scope, viewLocals);

        // update which direction
        self.update(registerData);

        // begin rendering and transitioning
        var enteringView = $ionicHistory.getViewById(registerData.viewId) || {};

        var renderStart = (disableRenderStartViewId !== registerData.viewId);
        self.render(registerData, viewLocals, enteringView, leavingView, renderStart, true);
      };


      self.update = function (registerData) {
        // always reset that this is the primary navView
        isPrimary = true;

        // remember what direction this navView should use
        // this may get updated later by a child navView
        direction = registerData.direction;

        var parentNavViewCtrl = $element.parent().inheritedData('$ionNavViewController');
        if (parentNavViewCtrl) {
          // this navView is nested inside another one
          // update the parent to use this direction and not
          // the other it originally was set to

          // inform the parent navView that it is not the primary navView
          parentNavViewCtrl.isPrimary(false);

          if (direction === 'enter' || direction === 'exit') {
            // they're entering/exiting a history
            // find parent navViewController
            parentNavViewCtrl.direction(direction);

            if (direction === 'enter') {
              // reset the direction so this navView doesn't animate
              // because it's parent will
              direction = 'none';
            }
          }
        }
      };


      self.render = function (registerData, viewLocals, enteringView, leavingView, renderStart, renderEnd) {
        // register the view and figure out where it lives in the various
        // histories and nav stacks, along with how views should enter/leave
        var switcher = $ionicViewSwitcher.create(self, viewLocals, enteringView, leavingView, renderStart, renderEnd);

        // init the rendering of views for this navView directive
        switcher.init(registerData, function () {
          // the view is now compiled, in the dom and linked, now lets transition the views.
          // this uses a callback incase THIS nav-view has a nested nav-view, and after the NESTED
          // nav-view links, the NESTED nav-view would update which direction THIS nav-view should use

          // kick off the transition of views
          switcher.transition(self.direction(), registerData.enableBack, !disableAnimation);

          // reset private vars for next time
          disableRenderStartViewId = disableAnimation = null;
        });

      };


      self.beforeEnter = function (transitionData) {
        if (isPrimary) {
          // only update this nav-view's nav-bar if this is the primary nav-view
          navBarDelegate = transitionData.navBarDelegate;
          var associatedNavBarCtrl = getAssociatedNavBarCtrl();
          associatedNavBarCtrl && associatedNavBarCtrl.update(transitionData);
          navSwipeAttr('');
        }
      };


      self.activeEleId = function (eleId) {
        if (arguments.length) {
          activeEleId = eleId;
        }
        return activeEleId;
      };


      self.transitionEnd = function () {
        var viewElements = $element.children();
        var x, l, viewElement;

        for (x = 0, l = viewElements.length; x < l; x++) {
          viewElement = viewElements.eq(x);

          if (viewElement.data(DATA_ELE_IDENTIFIER) === activeEleId) {
            // this is the active element
            navViewAttr(viewElement, VIEW_STATUS_ACTIVE);

          } else if (navViewAttr(viewElement) === 'leaving' || navViewAttr(viewElement) === VIEW_STATUS_ACTIVE || navViewAttr(viewElement) === VIEW_STATUS_CACHED) {
            // this is a leaving element or was the former active element, or is an cached element
            if (viewElement.data(DATA_DESTROY_ELE) || viewElement.data(DATA_NO_CACHE)) {
              // this element shouldn't stay cached
              $ionicViewSwitcher.destroyViewEle(viewElement);

            } else {
              // keep in the DOM, mark as cached
              navViewAttr(viewElement, VIEW_STATUS_CACHED);

              // disconnect the leaving scope
              ionic.Utils.disconnectScope(viewElement.scope());
            }
          }
        }

        navSwipeAttr('');

        // ensure no scrolls have been left frozen
        if (self.isSwipeFreeze) {
          $ionicScrollDelegate.freezeAllScrolls(false);
        }
      };


      function onTabsLeave(ev, data) {
        var viewElements = $element.children();
        var viewElement, viewScope;

        for (var x = 0, l = viewElements.length; x < l; x++) {
          viewElement = viewElements.eq(x);
          if (navViewAttr(viewElement) == VIEW_STATUS_ACTIVE) {
            viewScope = viewElement.scope();
            viewScope && viewScope.$emit(ev.name.replace('Tabs', 'View'), data);
            viewScope && viewScope.$broadcast(ev.name.replace('Tabs', 'ParentView'), data);
            break;
          }
        }
      }


      self.cacheCleanup = function () {
        var viewElements = $element.children();
        for (var x = 0, l = viewElements.length; x < l; x++) {
          if (viewElements.eq(x).data(DATA_DESTROY_ELE)) {
            $ionicViewSwitcher.destroyViewEle(viewElements.eq(x));
          }
        }
      };


      self.clearCache = function (stateIds) {
        var viewElements = $element.children();
        var viewElement, viewScope, x, l, y, eleIdentifier;

        for (x = 0, l = viewElements.length; x < l; x++) {
          viewElement = viewElements.eq(x);

          if (stateIds) {
            eleIdentifier = viewElement.data(DATA_ELE_IDENTIFIER);

            for (y = 0; y < stateIds.length; y++) {
              if (eleIdentifier === stateIds[y]) {
                $ionicViewSwitcher.destroyViewEle(viewElement);
              }
            }
            continue;
          }

          if (navViewAttr(viewElement) == VIEW_STATUS_CACHED) {
            $ionicViewSwitcher.destroyViewEle(viewElement);

          } else if (navViewAttr(viewElement) == VIEW_STATUS_ACTIVE) {
            viewScope = viewElement.scope();
            viewScope && viewScope.$broadcast('$ionicView.clearCache');
          }

        }
      };


      self.getViewElements = function () {
        return $element.children();
      };


      self.appendViewElement = function (viewEle, viewLocals) {
        // compile the entering element and get the link function
        var linkFn = $compile(viewEle);

        $element.append(viewEle);

        var viewScope = $scope.$new();

        if (viewLocals && viewLocals.$$controller) {
          viewLocals.$scope = viewScope;
          var controller = $controller(viewLocals.$$controller, viewLocals);
          if (viewLocals.$$controllerAs) {
            viewScope[viewLocals.$$controllerAs] = controller;
          }
          $element.children().data('$ngControllerController', controller);
        }

        linkFn(viewScope);

        return viewScope;
      };


      self.title = function (val) {
        var associatedNavBarCtrl = getAssociatedNavBarCtrl();
        associatedNavBarCtrl && associatedNavBarCtrl.title(val);
      };


      /**
       * @ngdoc method
       * @name $ionicNavView#enableBackButton
       * @description Enable/disable if the back button can be shown or not. For
       * example, the very first view in the navigation stack would not have a
       * back view, so the back button would be disabled.
       */
      self.enableBackButton = function (shouldEnable) {
        var associatedNavBarCtrl = getAssociatedNavBarCtrl();
        associatedNavBarCtrl && associatedNavBarCtrl.enableBackButton(shouldEnable);
      };


      /**
       * @ngdoc method
       * @name $ionicNavView#showBackButton
       * @description Show/hide the nav bar active back button. If the back button
       * is not possible this will not force the back button to show. The
       * `enableBackButton()` method handles if a back button is even possible or not.
       */
      self.showBackButton = function (shouldShow) {
        var associatedNavBarCtrl = getAssociatedNavBarCtrl();
        if (associatedNavBarCtrl) {
          if (arguments.length) {
            return associatedNavBarCtrl.showActiveBackButton(shouldShow);
          }
          return associatedNavBarCtrl.showActiveBackButton();
        }
        return true;
      };


      self.showBar = function (val) {
        var associatedNavBarCtrl = getAssociatedNavBarCtrl();
        if (associatedNavBarCtrl) {
          if (arguments.length) {
            return associatedNavBarCtrl.showBar(val);
          }
          return associatedNavBarCtrl.showBar();
        }
        return true;
      };


      self.isPrimary = function (val) {
        if (arguments.length) {
          isPrimary = val;
        }
        return isPrimary;
      };


      self.direction = function (val) {
        if (arguments.length) {
          direction = val;
        }
        return direction;
      };


      self.initSwipeBack = function () {
        var swipeBackHitWidth = $ionicConfig.views.swipeBackHitWidth();
        var viewTransition, associatedNavBarCtrl, backView;
        var deregDragStart, deregDrag, deregRelease;
        var windowWidth, startDragX, dragPoints;
        var cancelData = {};

        function onDragStart(ev) {
          if (!isPrimary || !$ionicConfig.views.swipeBackEnabled() || $ionicSideMenuDelegate.isOpenRight()) return;


          startDragX = getDragX(ev);
          if (startDragX > swipeBackHitWidth) return;

          backView = $ionicHistory.backView();

          var currentView = $ionicHistory.currentView();

          if (!backView || backView.historyId !== currentView.historyId || currentView.canSwipeBack === false) return;

          if (!windowWidth) windowWidth = window.innerWidth;

          self.isSwipeFreeze = $ionicScrollDelegate.freezeAllScrolls(true);

          var registerData = {
            direction: 'back'
          };

          dragPoints = [];

          cancelData = {
            showBar: self.showBar(),
            showBackButton: self.showBackButton()
          };

          var switcher = $ionicViewSwitcher.create(self, registerData, backView, currentView, true, false);
          switcher.loadViewElements(registerData);
          switcher.render(registerData);

          viewTransition = switcher.transition('back', $ionicHistory.enabledBack(backView), true);

          associatedNavBarCtrl = getAssociatedNavBarCtrl();

          deregDrag = ionic.onGesture('drag', onDrag, $element[0]);
          deregRelease = ionic.onGesture('release', onRelease, $element[0]);
        }

        function onDrag(ev) {
          if (isPrimary && viewTransition) {
            var dragX = getDragX(ev);

            dragPoints.push({
              t: Date.now(),
              x: dragX
            });

            if (dragX >= windowWidth - 15) {
              onRelease(ev);

            } else {
              var step = Math.min(Math.max(getSwipeCompletion(dragX), 0), 1);
              viewTransition.run(step);
              associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.run(step);
            }

          }
        }

        function onRelease(ev) {
          if (isPrimary && viewTransition && dragPoints && dragPoints.length > 1) {

            var now = Date.now();
            var releaseX = getDragX(ev);
            var startDrag = dragPoints[dragPoints.length - 1];

            for (var x = dragPoints.length - 2; x >= 0; x--) {
              if (now - startDrag.t > 200) {
                break;
              }
              startDrag = dragPoints[x];
            }

            var isSwipingRight = (releaseX >= dragPoints[dragPoints.length - 2].x);
            var releaseSwipeCompletion = getSwipeCompletion(releaseX);
            var velocity = Math.abs(startDrag.x - releaseX) / (now - startDrag.t);

            // private variables because ui-router has no way to pass custom data using $state.go
            disableRenderStartViewId = backView.viewId;
            disableAnimation = (releaseSwipeCompletion < 0.03 || releaseSwipeCompletion > 0.97);

            if (isSwipingRight && (releaseSwipeCompletion > 0.5 || velocity > 0.1)) {
              // complete view transition on release
              var speed = (velocity > 0.5 || velocity < 0.05 || releaseX > windowWidth - 45) ? 'fast' : 'slow';
              navSwipeAttr(disableAnimation ? '' : speed);
              backView.go();
              associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.complete(!disableAnimation, speed);

            } else {
              // cancel view transition on release
              navSwipeAttr(disableAnimation ? '' : 'fast');
              disableRenderStartViewId = null;
              viewTransition.cancel(!disableAnimation);
              associatedNavBarCtrl && associatedNavBarCtrl.activeTransition && associatedNavBarCtrl.activeTransition.cancel(!disableAnimation, 'fast', cancelData);
              disableAnimation = null;
            }

          }

          ionic.offGesture(deregDrag, 'drag', onDrag);
          ionic.offGesture(deregRelease, 'release', onRelease);

          windowWidth = viewTransition = dragPoints = null;

          self.isSwipeFreeze = $ionicScrollDelegate.freezeAllScrolls(false);
        }

        function getDragX(ev) {
          return ionic.tap.pointerCoord(ev.gesture.srcEvent).x;
        }

        function getSwipeCompletion(dragX) {
          return (dragX - startDragX) / windowWidth;
        }

        deregDragStart = ionic.onGesture('dragstart', onDragStart, $element[0]);

        $scope.$on('$destroy', function () {
          ionic.offGesture(deregDragStart, 'dragstart', onDragStart);
          ionic.offGesture(deregDrag, 'drag', onDrag);
          ionic.offGesture(deregRelease, 'release', onRelease);
          self.element = viewTransition = associatedNavBarCtrl = null;
        });
      };


      function navSwipeAttr(val) {
        ionic.DomUtil.cachedAttr($element, 'nav-swipe', val);
      }


      function onTabsTop(ev, isTabsTop) {
        var associatedNavBarCtrl = getAssociatedNavBarCtrl();
        associatedNavBarCtrl && associatedNavBarCtrl.hasTabsTop(isTabsTop);
      }

      function onBarSubheader(ev, isBarSubheader) {
        var associatedNavBarCtrl = getAssociatedNavBarCtrl();
        associatedNavBarCtrl && associatedNavBarCtrl.hasBarSubheader(isBarSubheader);
      }

      function getAssociatedNavBarCtrl() {
        if (navBarDelegate) {
          for (var x = 0; x < $ionicNavBarDelegate._instances.length; x++) {
            if ($ionicNavBarDelegate._instances[x].$$delegateHandle == navBarDelegate) {
              return $ionicNavBarDelegate._instances[x];
            }
          }
        }
        return $element.inheritedData('$ionNavBarController');
      }

    }]);

  IonicModule
  .controller('$ionicRefresher', [
    '$scope',
    '$attrs',
    '$element',
    '$ionicBind',
    '$timeout',
    function ($scope, $attrs, $element, $ionicBind, $timeout) {
      var self = this,
        isDragging = false,
        isOverscrolling = false,
        dragOffset = 0,
        lastOverscroll = 0,
        ptrThreshold = 60,
        activated = false,
        scrollTime = 500,
        startY = null,
        deltaY = null,
        canOverscroll = true,
        scrollParent,
        scrollChild;

      if (!isDefined($attrs.pullingIcon)) {
        $attrs.$set('pullingIcon', 'ion-android-arrow-down');
      }

      $scope.showSpinner = !isDefined($attrs.refreshingIcon) && $attrs.spinner != 'none';

      $scope.showIcon = isDefined($attrs.refreshingIcon);

      $ionicBind($scope, $attrs, {
        pullingIcon: '@',
        pullingText: '@',
        refreshingIcon: '@',
        refreshingText: '@',
        spinner: '@',
        disablePullingRotation: '@',
        $onRefresh: '&onRefresh',
        $onPulling: '&onPulling'
      });

      function handleMousedown(e) {
        e.touches = e.touches || [{
          screenX: e.screenX,
          screenY: e.screenY
        }];
        // Mouse needs this
        startY = Math.floor(e.touches[0].screenY);
      }

      function handleTouchstart(e) {
        e.touches = e.touches || [{
          screenX: e.screenX,
          screenY: e.screenY
        }];

        startY = e.touches[0].screenY;
      }

      function handleTouchend() {
        // reset Y
        startY = null;
        // if this wasn't an overscroll, get out immediately
        if (!canOverscroll && !isDragging) {
          return;
        }
        // the user has overscrolled but went back to native scrolling
        if (!isDragging) {
          dragOffset = 0;
          isOverscrolling = false;
          setScrollLock(false);
        } else {
          isDragging = false;
          dragOffset = 0;

          // the user has scroll far enough to trigger a refresh
          if (lastOverscroll > ptrThreshold) {
            start();
            scrollTo(ptrThreshold, scrollTime);

            // the user has overscrolled but not far enough to trigger a refresh
          } else {
            scrollTo(0, scrollTime, deactivate);
            isOverscrolling = false;
          }
        }
      }

      function handleTouchmove(e) {
        e.touches = e.touches || [{
          screenX: e.screenX,
          screenY: e.screenY
        }];

        // Force mouse events to have had a down event first
        if (!startY && e.type == 'mousemove') {
          return;
        }

        // if multitouch or regular scroll event, get out immediately
        if (!canOverscroll || e.touches.length > 1) {
          return;
        }
        //if this is a new drag, keep track of where we start
        if (startY === null) {
          startY = e.touches[0].screenY;
        }

        deltaY = e.touches[0].screenY - startY;

        // how far have we dragged so far?
        // kitkat fix for touchcancel events http://updates.html5rocks.com/2014/05/A-More-Compatible-Smoother-Touch
        // Only do this if we're not on crosswalk
        if (ionic.Platform.isAndroid() && ionic.Platform.version() === 4.4 && !ionic.Platform.isCrosswalk() && scrollParent.scrollTop === 0 && deltaY > 0) {
          isDragging = true;
          e.preventDefault();
        }


        // if we've dragged up and back down in to native scroll territory
        if (deltaY - dragOffset <= 0 || scrollParent.scrollTop !== 0) {

          if (isOverscrolling) {
            isOverscrolling = false;
            setScrollLock(false);
          }

          if (isDragging) {
            nativescroll(scrollParent, deltaY - dragOffset * -1);
          }

          // if we're not at overscroll 0 yet, 0 out
          if (lastOverscroll !== 0) {
            overscroll(0);
          }
          return;

        } else if (deltaY > 0 && scrollParent.scrollTop === 0 && !isOverscrolling) {
          // starting overscroll, but drag started below scrollTop 0, so we need to offset the position
          dragOffset = deltaY;
        }

        // prevent native scroll events while overscrolling
        e.preventDefault();

        // if not overscrolling yet, initiate overscrolling
        if (!isOverscrolling) {
          isOverscrolling = true;
          setScrollLock(true);
        }

        isDragging = true;
        // overscroll according to the user's drag so far
        overscroll((deltaY - dragOffset) / 3);

        // update the icon accordingly
        if (!activated && lastOverscroll > ptrThreshold) {
          activated = true;
          ionic.requestAnimationFrame(activate);

        } else if (activated && lastOverscroll < ptrThreshold) {
          activated = false;
          ionic.requestAnimationFrame(deactivate);
        }
      }

      function handleScroll(e) {
        // canOverscrol is used to greatly simplify the drag handler during normal scrolling
        canOverscroll = (e.target.scrollTop === 0) || isDragging;
      }

      function overscroll(val) {
        scrollChild.style[ionic.CSS.TRANSFORM] = 'translate3d(0px, ' + val + 'px, 0px)';
        lastOverscroll = val;
      }

      function nativescroll(target, newScrollTop) {
        // creates a scroll event that bubbles, can be cancelled, and with its view
        // and detail property initialized to window and 1, respectively
        target.scrollTop = newScrollTop;
        var e = document.createEvent("UIEvents");
        e.initUIEvent("scroll", true, true, window, 1);
        target.dispatchEvent(e);
      }

      function setScrollLock(enabled) {
        // set the scrollbar to be position:fixed in preparation to overscroll
        // or remove it so the app can be natively scrolled
        if (enabled) {
          ionic.requestAnimationFrame(function () {
            scrollChild.classList.add('overscroll');
            show();
          });

        } else {
          ionic.requestAnimationFrame(function () {
            scrollChild.classList.remove('overscroll');
            hide();
            deactivate();
          });
        }
      }

      $scope.$on('scroll.refreshComplete', function () {
        // prevent the complete from firing before the scroll has started
        $timeout(function () {

          ionic.requestAnimationFrame(tail);

          // scroll back to home during tail animation
          scrollTo(0, scrollTime, deactivate);

          // return to native scrolling after tail animation has time to finish
          $timeout(function () {

            if (isOverscrolling) {
              isOverscrolling = false;
              setScrollLock(false);
            }

          }, scrollTime);

        }, scrollTime);
      });

      function scrollTo(Y, duration, callback) {
        // scroll animation loop w/ easing
        // credit https://gist.github.com/dezinezync/5487119
        var start = Date.now(),
          from = lastOverscroll;

        if (from === Y) {
          callback();
          return; /* Prevent scrolling to the Y point if already there */
        }

        // decelerating to zero velocity
        function easeOutCubic(t) {
          return (--t) * t * t + 1;
        }

        // scroll loop
        function scroll() {
          var currentTime = Date.now(),
            time = Math.min(1, ((currentTime - start) / duration)),
            // where .5 would be 50% of time on a linear scale easedT gives a
            // fraction based on the easing method
            easedT = easeOutCubic(time);

          overscroll(Math.floor((easedT * (Y - from)) + from));

          if (time < 1) {
            ionic.requestAnimationFrame(scroll);

          } else {

            if (Y < 5 && Y > -5) {
              isOverscrolling = false;
              setScrollLock(false);
            }

            callback && callback();
          }
        }

        // start scroll loop
        ionic.requestAnimationFrame(scroll);
      }


      var touchStartEvent, touchMoveEvent, touchEndEvent;
      if (window.navigator.pointerEnabled) {
        touchStartEvent = 'pointerdown';
        touchMoveEvent = 'pointermove';
        touchEndEvent = 'pointerup';
      } else if (window.navigator.msPointerEnabled) {
        touchStartEvent = 'MSPointerDown';
        touchMoveEvent = 'MSPointerMove';
        touchEndEvent = 'MSPointerUp';
      } else {
        touchStartEvent = 'touchstart';
        touchMoveEvent = 'touchmove';
        touchEndEvent = 'touchend';
      }

      self.init = function () {
        scrollParent = $element.parent().parent()[0];
        scrollChild = $element.parent()[0];

        if (!scrollParent || !scrollParent.classList.contains('ionic-scroll') ||
          !scrollChild || !scrollChild.classList.contains('scroll')) {
          throw new Error('Refresher must be immediate child of ion-content or ion-scroll');
        }


        ionic.on(touchStartEvent, handleTouchstart, scrollChild);
        ionic.on(touchMoveEvent, handleTouchmove, scrollChild);
        ionic.on(touchEndEvent, handleTouchend, scrollChild);
        ionic.on('mousedown', handleMousedown, scrollChild);
        ionic.on('mousemove', handleTouchmove, scrollChild);
        ionic.on('mouseup', handleTouchend, scrollChild);
        ionic.on('scroll', handleScroll, scrollParent);

        // cleanup when done
        $scope.$on('$destroy', destroy);
      };

      function destroy() {
        if (scrollChild) {
          ionic.off(touchStartEvent, handleTouchstart, scrollChild);
          ionic.off(touchMoveEvent, handleTouchmove, scrollChild);
          ionic.off(touchEndEvent, handleTouchend, scrollChild);
          ionic.off('mousedown', handleMousedown, scrollChild);
          ionic.off('mousemove', handleTouchmove, scrollChild);
          ionic.off('mouseup', handleTouchend, scrollChild);
        }
        if (scrollParent) {
          ionic.off('scroll', handleScroll, scrollParent);
        }
        scrollParent = null;
        scrollChild = null;
      }

      // DOM manipulation and broadcast methods shared by JS and Native Scrolling
      // getter used by JS Scrolling
      self.getRefresherDomMethods = function () {
        return {
          activate: activate,
          deactivate: deactivate,
          start: start,
          show: show,
          hide: hide,
          tail: tail
        };
      };

      function activate() {
        $element[0].classList.add('active');
        $scope.$onPulling();
      }

      function deactivate() {
        // give tail 150ms to finish
        $timeout(function () {
          // deactivateCallback
          $element.removeClass('active refreshing refreshing-tail');
          if (activated) activated = false;
        }, 150);
      }

      function start() {
        // startCallback
        $element[0].classList.add('refreshing');
        var q = $scope.$onRefresh();

        if (q && q.then) {
          q['finally'](function () {
            $scope.$broadcast('scroll.refreshComplete');
          });
        }
      }

      function show() {
        // showCallback
        $element[0].classList.remove('invisible');
      }

      function hide() {
        // showCallback
        $element[0].classList.add('invisible');
      }

      function tail() {
        // tailCallback
        $element[0].classList.add('refreshing-tail');
      }

      // for testing
      self.__handleTouchmove = handleTouchmove;
      self.__getScrollChild = function () {
        return scrollChild;
      };
      self.__getScrollParent = function () {
        return scrollParent;
      };
    }
  ]);

  /**
   * @private
   */
  IonicModule

  .controller('$ionicScroll', [
    '$scope',
    'scrollViewOptions',
    '$timeout',
    '$window',
    '$location',
    '$document',
    '$ionicScrollDelegate',
    '$ionicHistory',
    function ($scope,
              scrollViewOptions,
              $timeout,
              $window,
              $location,
              $document,
              $ionicScrollDelegate,
              $ionicHistory) {

      var self = this;
      // for testing
      self.__timeout = $timeout;

      self._scrollViewOptions = scrollViewOptions; //for testing
      self.isNative = function () {
        return !!scrollViewOptions.nativeScrolling;
      };

      var element = self.element = scrollViewOptions.el;
      var $element = self.$element = jqLite(element);
      var scrollView;
      if (self.isNative()) {
        scrollView = self.scrollView = new ionic.views.ScrollNative(scrollViewOptions);
      } else {
        scrollView = self.scrollView = new ionic.views.Scroll(scrollViewOptions);
      }


      //Attach self to element as a controller so other directives can require this controller
      //through `require: '$ionicScroll'
      //Also attach to parent so that sibling elements can require this
      ($element.parent().length ? $element.parent() : $element)
      .data('$$ionicScrollController', self);

      var deregisterInstance = $ionicScrollDelegate._registerInstance(
        self, scrollViewOptions.delegateHandle, function () {
          return $ionicHistory.isActiveScope($scope);
        }
      );

      if (!isDefined(scrollViewOptions.bouncing)) {
        ionic.Platform.ready(function () {
          if (scrollView && scrollView.options) {
            scrollView.options.bouncing = true;
            if (ionic.Platform.isAndroid()) {
              // No bouncing by default on Android
              scrollView.options.bouncing = false;
              // Faster scroll decel
              scrollView.options.deceleration = 0.95;
            }
          }
        });
      }

      var resize = angular.bind(scrollView, scrollView.resize);
      angular.element($window).on('resize', resize);

      var scrollFunc = function (e) {
        var detail = (e.originalEvent || e).detail || {};
        $scope.$onScroll && $scope.$onScroll({
          event: e,
          scrollTop: detail.scrollTop || 0,
          scrollLeft: detail.scrollLeft || 0
        });
      };

      $element.on('scroll', scrollFunc);

      $scope.$on('$destroy', function () {
        deregisterInstance();
        scrollView && scrollView.__cleanup && scrollView.__cleanup();
        angular.element($window).off('resize', resize);
        if ($element) {
          $element.off('scroll', scrollFunc);
        }
        if (self._scrollViewOptions) {
          self._scrollViewOptions.el = null;
        }
        if (scrollViewOptions) {
          scrollViewOptions.el = null;
        }

        scrollView = self.scrollView = scrollViewOptions = self._scrollViewOptions = element = self.$element = $element = null;
      });

      $timeout(function () {
        scrollView && scrollView.run && scrollView.run();
      });

      self.getScrollView = function () {
        return scrollView;
      };

      self.getScrollPosition = function () {
        return scrollView.getValues();
      };

      self.resize = function () {
        return $timeout(resize, 0, false).then(function () {
          $element && $element.triggerHandler('scroll-resize');
        });
      };

      self.scrollTop = function (shouldAnimate) {
        self.resize().then(function () {
          if (!scrollView) {
            return;
          }
          scrollView.scrollTo(0, 0, !!shouldAnimate);
        });
      };

      self.scrollBottom = function (shouldAnimate) {
        self.resize().then(function () {
          if (!scrollView) {
            return;
          }
          var max = scrollView.getScrollMax();
          scrollView.scrollTo(max.left, max.top, !!shouldAnimate);
        });
      };

      self.scrollTo = function (left, top, shouldAnimate) {
        self.resize().then(function () {
          if (!scrollView) {
            return;
          }
          scrollView.scrollTo(left, top, !!shouldAnimate);
        });
      };

      self.zoomTo = function (zoom, shouldAnimate, originLeft, originTop) {
        self.resize().then(function () {
          if (!scrollView) {
            return;
          }
          scrollView.zoomTo(zoom, !!shouldAnimate, originLeft, originTop);
        });
      };

      self.zoomBy = function (zoom, shouldAnimate, originLeft, originTop) {
        self.resize().then(function () {
          if (!scrollView) {
            return;
          }
          scrollView.zoomBy(zoom, !!shouldAnimate, originLeft, originTop);
        });
      };

      self.scrollBy = function (left, top, shouldAnimate) {
        self.resize().then(function () {
          if (!scrollView) {
            return;
          }
          scrollView.scrollBy(left, top, !!shouldAnimate);
        });
      };

      self.anchorScroll = function (shouldAnimate) {
        self.resize().then(function () {
          if (!scrollView) {
            return;
          }
          var hash = $location.hash();
          var elm = hash && $document[0].getElementById(hash);
          if (!(hash && elm)) {
            scrollView.scrollTo(0, 0, !!shouldAnimate);
            return;
          }
          var curElm = elm;
          var scrollLeft = 0, scrollTop = 0;
          do {
            if (curElm !== null) scrollLeft += curElm.offsetLeft;
            if (curElm !== null) scrollTop += curElm.offsetTop;
            curElm = curElm.offsetParent;
          } while (curElm.attributes != self.element.attributes && curElm.offsetParent);
          scrollView.scrollTo(scrollLeft, scrollTop, !!shouldAnimate);
        });
      };

      self.freezeScroll = scrollView.freeze;
      self.freezeScrollShut = scrollView.freezeShut;

      self.freezeAllScrolls = function (shouldFreeze) {
        for (var i = 0; i < $ionicScrollDelegate._instances.length; i++) {
          $ionicScrollDelegate._instances[i].freezeScroll(shouldFreeze);
        }
      };


      /**
       * @private
       */
      self._setRefresher = function (refresherScope, refresherElement, refresherMethods) {
        self.refresher = refresherElement;
        var refresherHeight = self.refresher.clientHeight || 60;
        scrollView.activatePullToRefresh(
          refresherHeight,
          refresherMethods
        );
      };

    }]);

  IonicModule
  .controller('$ionicSideMenus', [
    '$scope',
    '$attrs',
    '$ionicSideMenuDelegate',
    '$ionicPlatform',
    '$ionicBody',
    '$ionicHistory',
    '$ionicScrollDelegate',
    'IONIC_BACK_PRIORITY',
    '$rootScope',
    function ($scope, $attrs, $ionicSideMenuDelegate, $ionicPlatform, $ionicBody, $ionicHistory, $ionicScrollDelegate, IONIC_BACK_PRIORITY, $rootScope) {
      var self = this;
      var rightShowing, leftShowing, isDragging;
      var startX, lastX, offsetX, isAsideExposed;
      var enableMenuWithBackViews = true;

      self.$scope = $scope;

      self.initialize = function (options) {
        self.left = options.left;
        self.right = options.right;
        self.setContent(options.content);
        self.dragThresholdX = options.dragThresholdX || 10;
        $ionicHistory.registerHistory(self.$scope);
      };

      /**
       * Set the content view controller if not passed in the constructor options.
       *
       * @param {object} content
       */
      self.setContent = function (content) {
        if (content) {
          self.content = content;

          self.content.onDrag = function (e) {
            self._handleDrag(e);
          };

          self.content.endDrag = function (e) {
            self._endDrag(e);
          };
        }
      };

      self.isOpenLeft = function () {
        return self.getOpenAmount() > 0;
      };

      self.isOpenRight = function () {
        return self.getOpenAmount() < 0;
      };

      /**
       * Toggle the left menu to open 100%
       */
      self.toggleLeft = function (shouldOpen) {
        if (isAsideExposed || !self.left.isEnabled) return;
        var openAmount = self.getOpenAmount();
        if (arguments.length === 0) {
          shouldOpen = openAmount <= 0;
        }
        self.content.enableAnimation();
        if (!shouldOpen) {
          self.openPercentage(0);
          $rootScope.$emit('$ionicSideMenuClose', 'left');
        } else {
          self.openPercentage(100);
          $rootScope.$emit('$ionicSideMenuOpen', 'left');
        }
      };

      /**
       * Toggle the right menu to open 100%
       */
      self.toggleRight = function (shouldOpen) {
        if (isAsideExposed || !self.right.isEnabled) return;
        var openAmount = self.getOpenAmount();
        if (arguments.length === 0) {
          shouldOpen = openAmount >= 0;
        }
        self.content.enableAnimation();
        if (!shouldOpen) {
          self.openPercentage(0);
          $rootScope.$emit('$ionicSideMenuClose', 'right');
        } else {
          self.openPercentage(-100);
          $rootScope.$emit('$ionicSideMenuOpen', 'right');
        }
      };

      self.toggle = function (side) {
        if (side == 'right') {
          self.toggleRight();
        } else {
          self.toggleLeft();
        }
      };

      /**
       * Close all menus.
       */
      self.close = function () {
        self.openPercentage(0);
        $rootScope.$emit('$ionicSideMenuClose', 'left');
        $rootScope.$emit('$ionicSideMenuClose', 'right');
      };

      /**
       * @return {float} The amount the side menu is open, either positive or negative for left (positive), or right (negative)
       */
      self.getOpenAmount = function () {
        return self.content && self.content.getTranslateX() || 0;
      };

      /**
       * @return {float} The ratio of open amount over menu width. For example, a
       * menu of width 100 open 50 pixels would be open 50% or a ratio of 0.5. Value is negative
       * for right menu.
       */
      self.getOpenRatio = function () {
        var amount = self.getOpenAmount();
        if (amount >= 0) {
          return amount / self.left.width;
        }
        return amount / self.right.width;
      };

      self.isOpen = function () {
        return self.getOpenAmount() !== 0;
      };

      /**
       * @return {float} The percentage of open amount over menu width. For example, a
       * menu of width 100 open 50 pixels would be open 50%. Value is negative
       * for right menu.
       */
      self.getOpenPercentage = function () {
        return self.getOpenRatio() * 100;
      };

      /**
       * Open the menu with a given percentage amount.
       * @param {float} percentage The percentage (positive or negative for left/right) to open the menu.
       */
      self.openPercentage = function (percentage) {
        var p = percentage / 100;

        if (self.left && percentage >= 0) {
          self.openAmount(self.left.width * p);
        } else if (self.right && percentage < 0) {
          self.openAmount(self.right.width * p);
        }

        // add the CSS class "menu-open" if the percentage does not
        // equal 0, otherwise remove the class from the body element
        $ionicBody.enableClass((percentage !== 0), 'menu-open');

        self.content.setCanScroll(percentage == 0);
      };

      /*
  function freezeAllScrolls(shouldFreeze) {
    if (shouldFreeze && !self.isScrollFreeze) {
      $ionicScrollDelegate.freezeAllScrolls(shouldFreeze);

    } else if (!shouldFreeze && self.isScrollFreeze) {
      $ionicScrollDelegate.freezeAllScrolls(false);
    }
    self.isScrollFreeze = shouldFreeze;
  }
  */

      /**
       * Open the menu the given pixel amount.
       * @param {float} amount the pixel amount to open the menu. Positive value for left menu,
       * negative value for right menu (only one menu will be visible at a time).
       */
      self.openAmount = function (amount) {
        var maxLeft = self.left && self.left.width || 0;
        var maxRight = self.right && self.right.width || 0;

        // Check if we can move to that side, depending if the left/right panel is enabled
        if (!(self.left && self.left.isEnabled) && amount > 0) {
          self.content.setTranslateX(0);
          return;
        }

        if (!(self.right && self.right.isEnabled) && amount < 0) {
          self.content.setTranslateX(0);
          return;
        }

        if (leftShowing && amount > maxLeft) {
          self.content.setTranslateX(maxLeft);
          return;
        }

        if (rightShowing && amount < -maxRight) {
          self.content.setTranslateX(-maxRight);
          return;
        }

        self.content.setTranslateX(amount);

        leftShowing = amount > 0;
        rightShowing = amount < 0;

        if (amount > 0) {
          // Push the z-index of the right menu down
          self.right && self.right.pushDown && self.right.pushDown();
          // Bring the z-index of the left menu up
          self.left && self.left.bringUp && self.left.bringUp();
        } else {
          // Bring the z-index of the right menu up
          self.right && self.right.bringUp && self.right.bringUp();
          // Push the z-index of the left menu down
          self.left && self.left.pushDown && self.left.pushDown();
        }
      };

      /**
       * Given an event object, find the final resting position of this side
       * menu. For example, if the user "throws" the content to the right and
       * releases the touch, the left menu should snap open (animated, of course).
       *
       * @param {Event} e the gesture event to use for snapping
       */
      self.snapToRest = function (e) {
        // We want to animate at the end of this
        self.content.enableAnimation();
        isDragging = false;

        // Check how much the panel is open after the drag, and
        // what the drag velocity is
        var ratio = self.getOpenRatio();

        if (ratio === 0) {
          // Just to be safe
          self.openPercentage(0);
          return;
        }

        var velocityThreshold = 0.3;
        var velocityX = e.gesture.velocityX;
        var direction = e.gesture.direction;

        // Going right, less than half, too slow (snap back)
        if (ratio > 0 && ratio < 0.5 && direction == 'right' && velocityX < velocityThreshold) {
          self.openPercentage(0);
        }

        // Going left, more than half, too slow (snap back)
        else if (ratio > 0.5 && direction == 'left' && velocityX < velocityThreshold) {
          self.openPercentage(100);
        }

        // Going left, less than half, too slow (snap back)
        else if (ratio < 0 && ratio > -0.5 && direction == 'left' && velocityX < velocityThreshold) {
          self.openPercentage(0);
        }

        // Going right, more than half, too slow (snap back)
        else if (ratio < 0.5 && direction == 'right' && velocityX < velocityThreshold) {
          self.openPercentage(-100);
        }

        // Going right, more than half, or quickly (snap open)
        else if (direction == 'right' && ratio >= 0 && (ratio >= 0.5 || velocityX > velocityThreshold)) {
          self.openPercentage(100);
        }

        // Going left, more than half, or quickly (span open)
        else if (direction == 'left' && ratio <= 0 && (ratio <= -0.5 || velocityX > velocityThreshold)) {
          self.openPercentage(-100);
        }

        // Snap back for safety
        else {
          self.openPercentage(0);
        }
      };

      self.enableMenuWithBackViews = function (val) {
        if (arguments.length) {
          enableMenuWithBackViews = !!val;
        }
        return enableMenuWithBackViews;
      };

      self.isAsideExposed = function () {
        return !!isAsideExposed;
      };

      self.exposeAside = function (shouldExposeAside) {
        if (!(self.left && self.left.isEnabled) && !(self.right && self.right.isEnabled)) return;
        self.close();

        isAsideExposed = shouldExposeAside;
        if ((self.left && self.left.isEnabled) && (self.right && self.right.isEnabled)) {
          self.content.setMarginLeftAndRight(isAsideExposed ? self.left.width : 0, isAsideExposed ? self.right.width : 0);
        } else if (self.left && self.left.isEnabled) {
          // set the left marget width if it should be exposed
          // otherwise set false so there's no left margin
          self.content.setMarginLeft(isAsideExposed ? self.left.width : 0);
        } else if (self.right && self.right.isEnabled) {
          self.content.setMarginRight(isAsideExposed ? self.right.width : 0);
        }
        self.$scope.$emit('$ionicExposeAside', isAsideExposed);
      };

      self.activeAsideResizing = function (isResizing) {
        $ionicBody.enableClass(isResizing, 'aside-resizing');
      };

      // End a drag with the given event
      self._endDrag = function (e) {
        if (isAsideExposed) return;

        if (isDragging) {
          self.snapToRest(e);
        }
        startX = null;
        lastX = null;
        offsetX = null;
      };

      // Handle a drag event
      self._handleDrag = function (e) {
        if (isAsideExposed || !$scope.dragContent) return;

        // If we don't have start coords, grab and store them
        if (!startX) {
          startX = e.gesture.touches[0].pageX;
          lastX = startX;
        } else {
          // Grab the current tap coords
          lastX = e.gesture.touches[0].pageX;
        }

        // Calculate difference from the tap points
        if (!isDragging && Math.abs(lastX - startX) > self.dragThresholdX) {
          // if the difference is greater than threshold, start dragging using the current
          // point as the starting point
          startX = lastX;

          isDragging = true;
          // Initialize dragging
          self.content.disableAnimation();
          offsetX = self.getOpenAmount();
        }

        if (isDragging) {
          self.openAmount(offsetX + (lastX - startX));
          //self.content.setCanScroll(false);
        }
      };

      self.canDragContent = function (canDrag) {
        if (arguments.length) {
          $scope.dragContent = !!canDrag;
        }
        return $scope.dragContent;
      };

      self.edgeThreshold = 25;
      self.edgeThresholdEnabled = false;
      self.edgeDragThreshold = function (value) {
        if (arguments.length) {
          if (isNumber(value) && value > 0) {
            self.edgeThreshold = value;
            self.edgeThresholdEnabled = true;
          } else {
            self.edgeThresholdEnabled = !!value;
          }
        }
        return self.edgeThresholdEnabled;
      };

      self.isDraggableTarget = function (e) {
        //Only restrict edge when sidemenu is closed and restriction is enabled
        var shouldOnlyAllowEdgeDrag = self.edgeThresholdEnabled && !self.isOpen();
        var startX = e.gesture.startEvent && e.gesture.startEvent.center &&
          e.gesture.startEvent.center.pageX;

        var dragIsWithinBounds = !shouldOnlyAllowEdgeDrag ||
          startX <= self.edgeThreshold ||
          startX >= self.content.element.offsetWidth - self.edgeThreshold;

        var backView = $ionicHistory.backView();
        var menuEnabled = enableMenuWithBackViews ? true : !backView;
        if (!menuEnabled) {
          var currentView = $ionicHistory.currentView() || {};
          return (dragIsWithinBounds && (backView.historyId !== currentView.historyId));
        }

        return ($scope.dragContent || self.isOpen()) &&
          dragIsWithinBounds &&
          !e.gesture.srcEvent.defaultPrevented &&
          menuEnabled &&
          !e.target.tagName.match(/input|textarea|select|object|embed/i) &&
          !e.target.isContentEditable &&
          !(e.target.dataset ? e.target.dataset.preventScroll : e.target.getAttribute('data-prevent-scroll') == 'true');
      };

      $scope.sideMenuContentTranslateX = 0;

      var deregisterBackButtonAction = noop;
      var closeSideMenu = angular.bind(self, self.close);

      $scope.$watch(function () {
        return self.getOpenAmount() !== 0;
      }, function (isOpen) {
        deregisterBackButtonAction();
        if (isOpen) {
          deregisterBackButtonAction = $ionicPlatform.registerBackButtonAction(
            closeSideMenu,
            IONIC_BACK_PRIORITY.sideMenu
          );
        }
      });

      var deregisterInstance = $ionicSideMenuDelegate._registerInstance(
        self, $attrs.delegateHandle, function () {
          return $ionicHistory.isActiveScope($scope);
        }
      );

      $scope.$on('$destroy', function () {
        deregisterInstance();
        deregisterBackButtonAction();
        self.$scope = null;
        if (self.content) {
          self.content.setCanScroll(true);
          self.content.element = null;
          self.content = null;
        }
      });

      self.initialize({
        left: {
          width: 275
        },
        right: {
          width: 275
        }
      });

    }]);

  (function (ionic) {

    var TRANSLATE32 = 'translate(32,32)';
    var STROKE_OPACITY = 'stroke-opacity';
    var ROUND = 'round';
    var INDEFINITE = 'indefinite';
    var DURATION = '750ms';
    var NONE = 'none';
    var SHORTCUTS = {
      a: 'animate',
      an: 'attributeName',
      at: 'animateTransform',
      c: 'circle',
      da: 'stroke-dasharray',
      os: 'stroke-dashoffset',
      f: 'fill',
      lc: 'stroke-linecap',
      rc: 'repeatCount',
      sw: 'stroke-width',
      t: 'transform',
      v: 'values'
    };

    var SPIN_ANIMATION = {
      v: '0,32,32;360,32,32',
      an: 'transform',
      type: 'rotate',
      rc: INDEFINITE,
      dur: DURATION
    };

    function createSvgElement(tagName, data, parent, spinnerName) {
      var ele = document.createElement(SHORTCUTS[tagName] || tagName);
      var k, x, y;

      for (k in data) {

        if (angular.isArray(data[k])) {
          for (x = 0; x < data[k].length; x++) {
            if (data[k][x].fn) {
              for (y = 0; y < data[k][x].t; y++) {
                createSvgElement(k, data[k][x].fn(y, spinnerName), ele, spinnerName);
              }
            } else {
              createSvgElement(k, data[k][x], ele, spinnerName);
            }
          }

        } else {
          setSvgAttribute(ele, k, data[k]);
        }
      }

      parent.appendChild(ele);
    }

    function setSvgAttribute(ele, k, v) {
      ele.setAttribute(SHORTCUTS[k] || k, v);
    }

    function animationValues(strValues, i) {
      var values = strValues.split(';');
      var back = values.slice(i);
      var front = values.slice(0, values.length - back.length);
      values = back.concat(front).reverse();
      return values.join(';') + ';' + values[0];
    }

    var IOS_SPINNER = {
      sw: 4,
      lc: ROUND,
      line: [{
        fn: function (i, spinnerName) {
          return {
            y1: spinnerName == 'ios' ? 17 : 12,
            y2: spinnerName == 'ios' ? 29 : 20,
            t: TRANSLATE32 + ' rotate(' + (30 * i + (i < 6 ? 180 : -180)) + ')',
            a: [{
              fn: function () {
                return {
                  an: STROKE_OPACITY,
                  dur: DURATION,
                  v: animationValues('0;.1;.15;.25;.35;.45;.55;.65;.7;.85;1', i),
                  rc: INDEFINITE
                };
              },
              t: 1
            }]
          };
        },
        t: 12
      }]
    };

    var spinners = {

      android: {
        c: [{
          sw: 6,
          da: 128,
          os: 82,
          r: 26,
          cx: 32,
          cy: 32,
          f: NONE
        }]
      },

      ios: IOS_SPINNER,

      'ios-small': IOS_SPINNER,

      bubbles: {
        sw: 0,
        c: [{
          fn: function (i) {
            return {
              cx: 24 * Math.cos(2 * Math.PI * i / 8),
              cy: 24 * Math.sin(2 * Math.PI * i / 8),
              t: TRANSLATE32,
              a: [{
                fn: function () {
                  return {
                    an: 'r',
                    dur: DURATION,
                    v: animationValues('1;2;3;4;5;6;7;8', i),
                    rc: INDEFINITE
                  };
                },
                t: 1
              }]
            };
          },
          t: 8
        }]
      },

      circles: {

        c: [{
          fn: function (i) {
            return {
              r: 5,
              cx: 24 * Math.cos(2 * Math.PI * i / 8),
              cy: 24 * Math.sin(2 * Math.PI * i / 8),
              t: TRANSLATE32,
              sw: 0,
              a: [{
                fn: function () {
                  return {
                    an: 'fill-opacity',
                    dur: DURATION,
                    v: animationValues('.3;.3;.3;.4;.7;.85;.9;1', i),
                    rc: INDEFINITE
                  };
                },
                t: 1
              }]
            };
          },
          t: 8
        }]
      },

      crescent: {
        c: [{
          sw: 4,
          da: 128,
          os: 82,
          r: 26,
          cx: 32,
          cy: 32,
          f: NONE,
          at: [SPIN_ANIMATION]
        }]
      },

      dots: {

        c: [{
          fn: function (i) {
            return {
              cx: 16 + (16 * i),
              cy: 32,
              sw: 0,
              a: [{
                fn: function () {
                  return {
                    an: 'fill-opacity',
                    dur: DURATION,
                    v: animationValues('.5;.6;.8;1;.8;.6;.5', i),
                    rc: INDEFINITE
                  };
                },
                t: 1
              }, {
                fn: function () {
                  return {
                    an: 'r',
                    dur: DURATION,
                    v: animationValues('4;5;6;5;4;3;3', i),
                    rc: INDEFINITE
                  };
                },
                t: 1
              }]
            };
          },
          t: 3
        }]
      },

      lines: {
        sw: 7,
        lc: ROUND,
        line: [{
          fn: function (i) {
            return {
              x1: 10 + (i * 14),
              x2: 10 + (i * 14),
              a: [{
                fn: function () {
                  return {
                    an: 'y1',
                    dur: DURATION,
                    v: animationValues('16;18;28;18;16', i),
                    rc: INDEFINITE
                  };
                },
                t: 1
              }, {
                fn: function () {
                  return {
                    an: 'y2',
                    dur: DURATION,
                    v: animationValues('48;44;36;46;48', i),
                    rc: INDEFINITE
                  };
                },
                t: 1
              }, {
                fn: function () {
                  return {
                    an: STROKE_OPACITY,
                    dur: DURATION,
                    v: animationValues('1;.8;.5;.4;1', i),
                    rc: INDEFINITE
                  };
                },
                t: 1
              }]
            };
          },
          t: 4
        }]
      },

      ripple: {
        f: NONE,
        'fill-rule': 'evenodd',
        sw: 3,
        circle: [{
          fn: function (i) {
            return {
              cx: 32,
              cy: 32,
              a: [{
                fn: function () {
                  return {
                    an: 'r',
                    begin: (i * -1) + 's',
                    dur: '2s',
                    v: '0;24',
                    keyTimes: '0;1',
                    keySplines: '0.1,0.2,0.3,1',
                    calcMode: 'spline',
                    rc: INDEFINITE
                  };
                },
                t: 1
              }, {
                fn: function () {
                  return {
                    an: STROKE_OPACITY,
                    begin: (i * -1) + 's',
                    dur: '2s',
                    v: '.2;1;.2;0',
                    rc: INDEFINITE
                  };
                },
                t: 1
              }]
            };
          },
          t: 2
        }]
      },

      spiral: {
        defs: [{
          linearGradient: [{
            id: 'sGD',
            gradientUnits: 'userSpaceOnUse',
            x1: 55, y1: 46, x2: 2, y2: 46,
            stop: [{
              offset: 0.1,
              class: 'stop1'
            }, {
              offset: 1,
              class: 'stop2'
            }]
          }]
        }],
        g: [{
          sw: 4,
          lc: ROUND,
          f: NONE,
          path: [{
            stroke: 'url(#sGD)',
            d: 'M4,32 c0,15,12,28,28,28c8,0,16-4,21-9'
          }, {
            d: 'M60,32 C60,16,47.464,4,32,4S4,16,4,32'
          }],
          at: [SPIN_ANIMATION]
        }]
      }

    };

    var animations = {

      android: function (ele) {
        // Note that this is called as a function, not a constructor.
        var self = {};

        this.stop = false;

        var rIndex = 0;
        var rotateCircle = 0;
        var startTime;
        var svgEle = ele.querySelector('g');
        var circleEle = ele.querySelector('circle');

        function run() {
          if (self.stop) return;

          var v = easeInOutCubic(Date.now() - startTime, 650);
          var scaleX = 1;
          var translateX = 0;
          var dasharray = (188 - (58 * v));
          var dashoffset = (182 - (182 * v));

          if (rIndex % 2) {
            scaleX = -1;
            translateX = -64;
            dasharray = (128 - (-58 * v));
            dashoffset = (182 * v);
          }

          var rotateLine = [0, -101, -90, -11, -180, 79, -270, -191][rIndex];

          setSvgAttribute(circleEle, 'da', Math.max(Math.min(dasharray, 188), 128));
          setSvgAttribute(circleEle, 'os', Math.max(Math.min(dashoffset, 182), 0));
          setSvgAttribute(circleEle, 't', 'scale(' + scaleX + ',1) translate(' + translateX + ',0) rotate(' + rotateLine + ',32,32)');

          rotateCircle += 4.1;
          if (rotateCircle > 359) rotateCircle = 0;
          setSvgAttribute(svgEle, 't', 'rotate(' + rotateCircle + ',32,32)');

          if (v >= 1) {
            rIndex++;
            if (rIndex > 7) rIndex = 0;
            startTime = Date.now();
          }

          ionic.requestAnimationFrame(run);
        }

        return function () {
          startTime = Date.now();
          run();
          return self;
        };

      }

    };

    function easeInOutCubic(t, c) {
      t /= c / 2;
      if (t < 1) return 1 / 2 * t * t * t;
      t -= 2;
      return 1 / 2 * (t * t * t + 2);
    }


    IonicModule
    .controller('$ionicSpinner', [
      '$element',
      '$attrs',
      '$ionicConfig',
      function ($element, $attrs, $ionicConfig) {
        var spinnerName, anim;

        this.init = function () {
          spinnerName = $attrs.icon || $ionicConfig.spinner.icon();

          var container = document.createElement('div');
          createSvgElement('svg', {
            viewBox: '0 0 64 64',
            g: [spinners[spinnerName]]
          }, container, spinnerName);

          // Specifically for animations to work,
          // Android 4.3 and below requires the element to be
          // added as an html string, rather than dynmically
          // building up the svg element and appending it.
          $element.html(container.innerHTML);

          this.start();

          return spinnerName;
        };

        this.start = function () {
          animations[spinnerName] && (anim = animations[spinnerName]($element[0])());
        };

        this.stop = function () {
          animations[spinnerName] && (anim.stop = true);
        };

      }]);

  })(ionic);

  IonicModule
  .controller('$ionicTab', [
    '$scope',
    '$ionicHistory',
    '$attrs',
    '$location',
    '$state',
    function ($scope, $ionicHistory, $attrs, $location, $state) {
      this.$scope = $scope;

      //All of these exposed for testing
      this.hrefMatchesState = function () {
        return $attrs.href && $location.path().indexOf(
          $attrs.href.replace(/^#/, '').replace(/\/$/, '')
        ) === 0;
      };
      this.srefMatchesState = function () {
        return $attrs.uiSref && $state.includes($attrs.uiSref.split('(')[0]);
      };
      this.navNameMatchesState = function () {
        return this.navViewName && $ionicHistory.isCurrentStateNavView(this.navViewName);
      };

      this.tabMatchesState = function () {
        return this.hrefMatchesState() || this.srefMatchesState() || this.navNameMatchesState();
      };
    }]);

  IonicModule
  .controller('$ionicTabs', [
    '$scope',
    '$element',
    '$ionicHistory',
    function ($scope, $element, $ionicHistory) {
      var self = this;
      var selectedTab = null;
      var previousSelectedTab = null;
      var selectedTabIndex;
      var isVisible = true;
      self.tabs = [];

      self.selectedIndex = function () {
        return self.tabs.indexOf(selectedTab);
      };
      self.selectedTab = function () {
        return selectedTab;
      };
      self.previousSelectedTab = function () {
        return previousSelectedTab;
      };

      self.add = function (tab) {
        $ionicHistory.registerHistory(tab);
        self.tabs.push(tab);
      };

      self.remove = function (tab) {
        var tabIndex = self.tabs.indexOf(tab);
        if (tabIndex === -1) {
          return;
        }
        //Use a field like '$tabSelected' so developers won't accidentally set it in controllers etc
        if (tab.$tabSelected) {
          self.deselect(tab);
          //Try to select a new tab if we're removing a tab
          if (self.tabs.length === 1) {
            //Do nothing if there are no other tabs to select
          } else {
            //Select previous tab if it's the last tab, else select next tab
            var newTabIndex = tabIndex === self.tabs.length - 1 ? tabIndex - 1 : tabIndex + 1;
            self.select(self.tabs[newTabIndex]);
          }
        }
        self.tabs.splice(tabIndex, 1);
      };

      self.deselect = function (tab) {
        if (tab.$tabSelected) {
          previousSelectedTab = selectedTab;
          selectedTab = selectedTabIndex = null;
          tab.$tabSelected = false;
          (tab.onDeselect || noop)();
          tab.$broadcast && tab.$broadcast('$ionicHistory.deselect');
        }
      };

      self.select = function (tab, shouldEmitEvent) {
        var tabIndex;
        if (isNumber(tab)) {
          tabIndex = tab;
          if (tabIndex >= self.tabs.length) return;
          tab = self.tabs[tabIndex];
        } else {
          tabIndex = self.tabs.indexOf(tab);
        }

        if (arguments.length === 1) {
          shouldEmitEvent = !!(tab.navViewName || tab.uiSref);
        }

        if (selectedTab && selectedTab.$historyId == tab.$historyId) {
          if (shouldEmitEvent) {
            $ionicHistory.goToHistoryRoot(tab.$historyId);
          }

        } else if (selectedTabIndex !== tabIndex) {
          forEach(self.tabs, function (tab) {
            self.deselect(tab);
          });

          selectedTab = tab;
          selectedTabIndex = tabIndex;

          if (self.$scope && self.$scope.$parent) {
            self.$scope.$parent.$activeHistoryId = tab.$historyId;
          }

          //Use a funny name like $tabSelected so the developer doesn't overwrite the var in a child scope
          tab.$tabSelected = true;
          (tab.onSelect || noop)();

          if (shouldEmitEvent) {
            $scope.$emit('$ionicHistory.change', {
              type: 'tab',
              tabIndex: tabIndex,
              historyId: tab.$historyId,
              navViewName: tab.navViewName,
              hasNavView: !!tab.navViewName,
              title: tab.title,
              url: tab.href,
              uiSref: tab.uiSref
            });
          }

          $scope.$broadcast("tabSelected", {selectedTab: tab, selectedTabIndex: tabIndex});
        }
      };

      self.hasActiveScope = function () {
        for (var x = 0; x < self.tabs.length; x++) {
          if ($ionicHistory.isActiveScope(self.tabs[x])) {
            return true;
          }
        }
        return false;
      };

      self.showBar = function (show) {
        if (arguments.length) {
          if (show) {
            $element.removeClass('tabs-item-hide');
          } else {
            $element.addClass('tabs-item-hide');
          }
          isVisible = !!show;
        }
        return isVisible;
      };
    }]);

  IonicModule
  .controller('$ionicView', [
    '$scope',
    '$element',
    '$attrs',
    '$compile',
    '$rootScope',
    function ($scope, $element, $attrs, $compile, $rootScope) {
      var self = this;
      var navElementHtml = {};
      var navViewCtrl;
      var navBarDelegateHandle;
      var hasViewHeaderBar;
      var deregisters = [];
      var viewTitle;

      var deregIonNavBarInit = $scope.$on('ionNavBar.init', function (ev, delegateHandle) {
        // this view has its own ion-nav-bar, remember the navBarDelegateHandle for this view
        ev.stopPropagation();
        navBarDelegateHandle = delegateHandle;
      });


      self.init = function () {
        deregIonNavBarInit();

        var modalCtrl = $element.inheritedData('$ionModalController');
        navViewCtrl = $element.inheritedData('$ionNavViewController');

        // don't bother if inside a modal or there's no parent navView
        if (!navViewCtrl || modalCtrl) return;

        // add listeners for when this view changes
        $scope.$on('$ionicView.beforeEnter', self.beforeEnter);
        $scope.$on('$ionicView.afterEnter', afterEnter);
        $scope.$on('$ionicView.beforeLeave', deregisterFns);
      };

      self.beforeEnter = function (ev, transData) {
        // this event was emitted, starting at intial ion-view, then bubbles up
        // only the first ion-view should do something with it, parent ion-views should ignore
        if (transData && !transData.viewNotified) {
          transData.viewNotified = true;

          if (!$rootScope.$$phase) $scope.$digest();
          viewTitle = isDefined($attrs.viewTitle) ? $attrs.viewTitle : $attrs.title;

          var navBarItems = {};
          for (var n in navElementHtml) {
            navBarItems[n] = generateNavBarItem(navElementHtml[n]);
          }

          navViewCtrl.beforeEnter(extend(transData, {
            title: viewTitle,
            showBack: !attrTrue('hideBackButton'),
            navBarItems: navBarItems,
            navBarDelegate: navBarDelegateHandle || null,
            showNavBar: !attrTrue('hideNavBar'),
            hasHeaderBar: !!hasViewHeaderBar
          }));

          // make sure any existing observers are cleaned up
          deregisterFns();
        }
      };


      function afterEnter() {
        // only listen for title updates after it has entered
        // but also deregister the observe before it leaves
        var viewTitleAttr = isDefined($attrs.viewTitle) && 'viewTitle' || isDefined($attrs.title) && 'title';
        if (viewTitleAttr) {
          titleUpdate($attrs[viewTitleAttr]);
          deregisters.push($attrs.$observe(viewTitleAttr, titleUpdate));
        }

        if (isDefined($attrs.hideBackButton)) {
          deregisters.push($scope.$watch($attrs.hideBackButton, function (val) {
            navViewCtrl.showBackButton(!val);
          }));
        }

        if (isDefined($attrs.hideNavBar)) {
          deregisters.push($scope.$watch($attrs.hideNavBar, function (val) {
            navViewCtrl.showBar(!val);
          }));
        }
      }


      function titleUpdate(newTitle) {
        if (isDefined(newTitle) && newTitle !== viewTitle) {
          viewTitle = newTitle;
          navViewCtrl.title(viewTitle);
        }
      }


      function deregisterFns() {
        // remove all existing $attrs.$observe's
        for (var x = 0; x < deregisters.length; x++) {
          deregisters[x]();
        }
        deregisters = [];
      }


      function generateNavBarItem(html) {
        if (html) {
          // every time a view enters we need to recreate its view buttons if they exist
          return $compile(html)($scope.$new());
        }
      }


      function attrTrue(key) {
        return !!$scope.$eval($attrs[key]);
      }


      self.navElement = function (type, html) {
        navElementHtml[type] = html;
      };

    }]);

  /*
 * We don't document the ionActionSheet directive, we instead document
 * the $ionicActionSheet service
 */
  IonicModule
  .directive('ionActionSheet', ['$document', function ($document) {
    return {
      restrict: 'E',
      scope: true,
      replace: true,
      link: function ($scope, $element) {

        var keyUp = function (e) {
          if (e.which == 27) {
            $scope.cancel();
            $scope.$apply();
          }
        };

        var backdropClick = function (e) {
          if (e.target == $element[0]) {
            $scope.cancel();
            $scope.$apply();
          }
        };
        $scope.$on('$destroy', function () {
          $element.remove();
          $document.unbind('keyup', keyUp);
        });

        $document.bind('keyup', keyUp);
        $element.bind('click', backdropClick);
      },
      template: '<div class="action-sheet-backdrop">' +
        '<div class="action-sheet-wrapper">' +
        '<div class="action-sheet" ng-class="{\'action-sheet-has-icons\': $actionSheetHasIcon}">' +
        '<div class="action-sheet-group action-sheet-options">' +
        '<div class="action-sheet-title" ng-if="titleText" ng-bind-html="titleText"></div>' +
        '<button class="button action-sheet-option" ng-click="buttonClicked($index)" ng-class="b.className" ng-repeat="b in buttons" ng-bind-html="b.text"></button>' +
        '<button class="button destructive action-sheet-destructive" ng-if="destructiveText" ng-click="destructiveButtonClicked()" ng-bind-html="destructiveText"></button>' +
        '</div>' +
        '<div class="action-sheet-group action-sheet-cancel" ng-if="cancelText">' +
        '<button class="button" ng-click="cancel()" ng-bind-html="cancelText"></button>' +
        '</div>' +
        '</div>' +
        '</div>' +
        '</div>'
    };
  }]);


  /**
   * @ngdoc directive
   * @name ionCheckbox
   * @module ionic
   * @restrict E
   * @codepen hqcju
   * @description
   * The checkbox is no different than the HTML checkbox input, except it's styled differently.
   *
   * The checkbox behaves like any [AngularJS checkbox](http://docs.angularjs.org/api/ng/input/input[checkbox]).
   *
   * @usage
   * ```html
   * <ion-checkbox ng-model="isChecked">Checkbox Label</ion-checkbox>
   * ```
   */

  IonicModule
  .directive('ionCheckbox', ['$ionicConfig', function ($ionicConfig) {
    return {
      restrict: 'E',
      replace: true,
      require: '?ngModel',
      transclude: true,
      template:
        '<label class="item item-checkbox">' +
        '<div class="checkbox checkbox-input-hidden disable-pointer-events">' +
        '<input type="checkbox">' +
        '<i class="checkbox-icon"></i>' +
        '</div>' +
        '<div class="item-content disable-pointer-events" ng-transclude></div>' +
        '</label>',
      compile: function (element, attr) {
        var input = element.find('input');
        forEach({
          'name': attr.name,
          'ng-value': attr.ngValue,
          'ng-model': attr.ngModel,
          'ng-checked': attr.ngChecked,
          'ng-disabled': attr.ngDisabled,
          'ng-true-value': attr.ngTrueValue,
          'ng-false-value': attr.ngFalseValue,
          'ng-change': attr.ngChange,
          'ng-required': attr.ngRequired,
          'required': attr.required
        }, function (value, name) {
          if (isDefined(value)) {
            input.attr(name, value);
          }
        });
        var checkboxWrapper = element[0].querySelector('.checkbox');
        checkboxWrapper.classList.add('checkbox-' + $ionicConfig.form.checkbox());
      }
    };
  }]);


  /**
   * @ngdoc directive
   * @restrict A
   * @name collectionRepeat
   * @module ionic
   * @codepen 7ec1ec58f2489ab8f359fa1a0fe89c15
   * @description
   * `collection-repeat` allows an app to show huge lists of items much more performantly than
   * `ng-repeat`.
   *
   * It renders into the DOM only as many items as are currently visible.
   *
   * This means that on a phone screen that can fit eight items, only the eight items matching
   * the current scroll position will be rendered.
   *
   * **The Basics**:
   *
   * - The data given to collection-repeat must be an array.
   * - If the `item-height` and `item-width` attributes are not supplied, it will be assumed that
   *   every item in the list has the same dimensions as the first item.
   * - Don't use angular one-time binding (`::`) with collection-repeat. The scope of each item is
   *   assigned new data and re-digested as you scroll. Bindings need to update, and one-time bindings
   *   won't.
   *
   * **Performance Tips**:
   *
   * - The iOS webview has a performance bottleneck when switching out `<img src>` attributes.
   *   To increase performance of images on iOS, cache your images in advance and,
   *   if possible, lower the number of unique images. We're working on [a solution](https://github.com/ionic-team/ionic/issues/3194).
   *
   * @usage
   * #### Basic Item List ([codepen](http://codepen.io/ionic/pen/0c2c35a34a8b18ad4d793fef0b081693))
   * ```html
   * <ion-content>
   *   <ion-item collection-repeat="item in items">
   *     {% raw %}{{item}}{% endraw %}
   *   </ion-item>
   * </ion-content>
   * ```
   *
   * #### Grid of Images ([codepen](http://codepen.io/ionic/pen/5515d4efd9d66f780e96787387f41664))
   * ```html
   * <ion-content>
   *   <img collection-repeat="photo in photos"
   *     item-width="33%"
   *     item-height="200px"
   *     ng-src="{% raw %}{{photo.url}}{% endraw %}">
   * </ion-content>
   * ```
   *
   * #### Horizontal Scroller, Dynamic Item Width ([codepen](http://codepen.io/ionic/pen/67cc56b349124a349acb57a0740e030e))
   * ```html
   * <ion-content>
   *   <h2>Available Kittens:</h2>
   *   <ion-scroll direction="x" class="available-scroller">
   *     <div class="photo" collection-repeat="photo in main.photos"
   *        item-height="250" item-width="photo.width + 30">
   *        <img ng-src="{% raw %}{{photo.src}}{% endraw %}">
   *     </div>
   *   </ion-scroll>
   * </ion-content>
   * ```
   *
   * @param {expression} collection-repeat The expression indicating how to enumerate a collection,
   *   of the format  `variable in expression` – where variable is the user defined loop variable
   *   and `expression` is a scope expression giving the collection to enumerate.
   *   For example: `album in artist.albums` or `album in artist.albums | orderBy:'name'`.
   * @param {expression=} item-width The width of the repeated element. The expression must return
   *   a number (pixels) or a percentage. Defaults to the width of the first item in the list.
   *   (previously named collection-item-width)
   * @param {expression=} item-height The height of the repeated element. The expression must return
   *   a number (pixels) or a percentage. Defaults to the height of the first item in the list.
   *   (previously named collection-item-height)
   * @param {number=} item-render-buffer The number of items to load before and after the visible
   *   items in the list. Default 3. Tip: set this higher if you have lots of images to preload, but
   *   don't set it too high or you'll see performance loss.
   * @param {boolean=} force-refresh-images Force images to refresh as you scroll. This fixes a problem
   *   where, when an element is interchanged as scrolling, its image will still have the old src
   *   while the new src loads. Setting this to true comes with a small performance loss.
   */

  IonicModule
  .directive('collectionRepeat', CollectionRepeatDirective)
  .factory('$ionicCollectionManager', RepeatManagerFactory);

  var ONE_PX_TRANSPARENT_IMG_SRC = '';
  var WIDTH_HEIGHT_REGEX = /height:.*?px;\s*width:.*?px/;
  var DEFAULT_RENDER_BUFFER = 3;

  CollectionRepeatDirective.$inject = ['$ionicCollectionManager', '$parse', '$window', '$$rAF', '$rootScope', '$timeout'];

  function CollectionRepeatDirective($ionicCollectionManager, $parse, $window, $$rAF, $rootScope, $timeout) {
    return {
      restrict: 'A',
      priority: 1000,
      transclude: 'element',
      $$tlb: true,
      require: '^^$ionicScroll',
      link: postLink
    };

    function postLink(scope, element, attr, scrollCtrl, transclude) {
      var scrollView = scrollCtrl.scrollView;
      var node = element[0];
      var containerNode = angular.element('<div class="collection-repeat-container">')[0];
      node.parentNode.replaceChild(containerNode, node);

      if (scrollView.options.scrollingX && scrollView.options.scrollingY) {
        throw new Error("collection-repeat expected a parent x or y scrollView, not " +
          "an xy scrollView.");
      }

      var repeatExpr = attr.collectionRepeat;
      var match = repeatExpr.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
      if (!match) {
        throw new Error("collection-repeat expected expression in form of '_item_ in " +
          "_collection_[ track by _id_]' but got '" + attr.collectionRepeat + "'.");
      }
      var keyExpr = match[1];
      var listExpr = match[2];
      var listGetter = $parse(listExpr);
      var heightData = {};
      var widthData = {};
      var computedStyleDimensions = {};
      var data = [];
      var repeatManager;

      // attr.collectionBufferSize is deprecated
      var renderBufferExpr = attr.itemRenderBuffer || attr.collectionBufferSize;
      var renderBuffer = angular.isDefined(renderBufferExpr) ?
        parseInt(renderBufferExpr) :
        DEFAULT_RENDER_BUFFER;

      // attr.collectionItemHeight is deprecated
      var heightExpr = attr.itemHeight || attr.collectionItemHeight;
      // attr.collectionItemWidth is deprecated
      var widthExpr = attr.itemWidth || attr.collectionItemWidth;

      var afterItemsContainer = initAfterItemsContainer();

      var changeValidator = makeChangeValidator();
      initDimensions();

      // Dimensions are refreshed on resize or data change.
      scrollCtrl.$element.on('scroll-resize', refreshDimensions);

      angular.element($window).on('resize', onResize);
      var unlistenToExposeAside = $rootScope.$on('$ionicExposeAside', ionic.animationFrameThrottle(function () {
        scrollCtrl.scrollView.resize();
        onResize();
      }));
      $timeout(refreshDimensions, 0, false);

      function onResize() {
        if (changeValidator.resizeRequiresRefresh(scrollView.__clientWidth, scrollView.__clientHeight)) {
          refreshDimensions();
        }
      }

      scope.$watchCollection(listGetter, function (newValue) {
        data = newValue || (newValue = []);
        if (!angular.isArray(newValue)) {
          throw new Error("collection-repeat expected an array for '" + listExpr + "', " +
            "but got a " + typeof value);
        }
        // Wait for this digest to end before refreshing everything.
        scope.$$postDigest(function () {
          getRepeatManager().setData(data);
          if (changeValidator.dataChangeRequiresRefresh(data)) refreshDimensions();
        });
      });

      scope.$on('$destroy', function () {
        angular.element($window).off('resize', onResize);
        unlistenToExposeAside();
        scrollCtrl.$element && scrollCtrl.$element.off('scroll-resize', refreshDimensions);

        computedStyleNode && computedStyleNode.parentNode &&
        computedStyleNode.parentNode.removeChild(computedStyleNode);
        computedStyleScope && computedStyleScope.$destroy();
        computedStyleScope = computedStyleNode = null;

        repeatManager && repeatManager.destroy();
        repeatManager = null;
      });

      function makeChangeValidator() {
        var self;
        return (self = {
          dataLength: 0,
          width: 0,
          height: 0,
          // A resize triggers a refresh only if we have data, the scrollView has size,
          // and the size has changed.
          resizeRequiresRefresh: function (newWidth, newHeight) {
            var requiresRefresh = self.dataLength && newWidth && newHeight &&
              (newWidth !== self.width || newHeight !== self.height);

            self.width = newWidth;
            self.height = newHeight;

            return !!requiresRefresh;
          },
          // A change in data only triggers a refresh if the data has length, or if the data's
          // length is less than before.
          dataChangeRequiresRefresh: function (newData) {
            var requiresRefresh = newData.length > 0 || newData.length < self.dataLength;

            self.dataLength = newData.length;

            return !!requiresRefresh;
          }
        });
      }

      function getRepeatManager() {
        return repeatManager || (repeatManager = new $ionicCollectionManager({
          afterItemsNode: afterItemsContainer[0],
          containerNode: containerNode,
          heightData: heightData,
          widthData: widthData,
          forceRefreshImages: !!(isDefined(attr.forceRefreshImages) && attr.forceRefreshImages !== 'false'),
          keyExpression: keyExpr,
          renderBuffer: renderBuffer,
          scope: scope,
          scrollView: scrollCtrl.scrollView,
          transclude: transclude
        }));
      }

      function initAfterItemsContainer() {
        var container = angular.element(
          scrollView.__content.querySelector('.collection-repeat-after-container')
        );
        // Put everything in the view after the repeater into a container.
        if (!container.length) {
          var elementIsAfterRepeater = false;
          var afterNodes = [].filter.call(scrollView.__content.childNodes, function (node) {
            if (ionic.DomUtil.contains(node, containerNode)) {
              elementIsAfterRepeater = true;
              return false;
            }
            return elementIsAfterRepeater;
          });
          container = angular.element('<span class="collection-repeat-after-container">');
          if (scrollView.options.scrollingX) {
            container.addClass('horizontal');
          }
          container.append(afterNodes);
          scrollView.__content.appendChild(container[0]);
        }
        return container;
      }

      function initDimensions() {
        //Height and width have four 'modes':
        //1) Computed Mode
        //  - Nothing is supplied, so we getComputedStyle() on one element in the list and use
        //    that width and height value for the width and height of every item. This is re-computed
        //    every resize.
        //2) Constant Mode, Static Integer
        //  - The user provides a constant number for width or height, in pixels. We parse it,
        //    store it on the `value` field, and it never changes
        //3) Constant Mode, Percent
        //  - The user provides a percent string for width or height. The getter for percent is
        //    stored on the `getValue()` field, and is re-evaluated once every resize. The result
        //    is stored on the `value` field.
        //4) Dynamic Mode
        //  - The user provides a dynamic expression for the width or height.  This is re-evaluated
        //    for every item, stored on the `.getValue()` field.
        if (heightExpr) {
          parseDimensionAttr(heightExpr, heightData);
        } else {
          heightData.computed = true;
        }
        if (widthExpr) {
          parseDimensionAttr(widthExpr, widthData);
        } else {
          widthData.computed = true;
        }
      }

      function refreshDimensions() {
        var hasData = data.length > 0;

        if (hasData && (heightData.computed || widthData.computed)) {
          computeStyleDimensions();
        }

        if (hasData && heightData.computed) {
          heightData.value = computedStyleDimensions.height;
          if (!heightData.value) {
            throw new Error('collection-repeat tried to compute the height of repeated elements "' +
              repeatExpr + '", but was unable to. Please provide the "item-height" attribute. ' +
              'http://ionicframework.com/docs/api/directive/collectionRepeat/');
          }
        } else if (!heightData.dynamic && heightData.getValue) {
          // If it's a constant with a getter (eg percent), we just refresh .value after resize
          heightData.value = heightData.getValue();
        }

        if (hasData && widthData.computed) {
          widthData.value = computedStyleDimensions.width;
          if (!widthData.value) {
            throw new Error('collection-repeat tried to compute the width of repeated elements "' +
              repeatExpr + '", but was unable to. Please provide the "item-width" attribute. ' +
              'http://ionicframework.com/docs/api/directive/collectionRepeat/');
          }
        } else if (!widthData.dynamic && widthData.getValue) {
          // If it's a constant with a getter (eg percent), we just refresh .value after resize
          widthData.value = widthData.getValue();
        }
        // Dynamic dimensions aren't updated on resize. Since they're already dynamic anyway,
        // .getValue() will be used.

        getRepeatManager().refreshLayout();
      }

      function parseDimensionAttr(attrValue, dimensionData) {
        if (!attrValue) return;

        var parsedValue;
        // Try to just parse the plain attr value
        try {
          parsedValue = $parse(attrValue);
        } catch (e) {
          // If the parse fails and the value has `px` or `%` in it, surround the attr in
          // quotes, to attempt to let the user provide a simple `attr="100%"` or `attr="100px"`
          if (attrValue.trim().match(/\d+(px|%)$/)) {
            attrValue = '"' + attrValue + '"';
          }
          parsedValue = $parse(attrValue);
        }

        var constantAttrValue = attrValue.replace(/(\'|\"|px|%)/g, '').trim();
        var isConstant = constantAttrValue.length && !/([a-zA-Z]|\$|:|\?)/.test(constantAttrValue);
        dimensionData.attrValue = attrValue;

        // If it's a constant, it's either a percent or just a constant pixel number.
        if (isConstant) {
          // For percents, store the percent getter on .getValue()
          if (attrValue.indexOf('%') > -1) {
            var decimalValue = parseFloat(parsedValue()) / 100;
            dimensionData.getValue = dimensionData === heightData ?
              function () {
                return Math.floor(decimalValue * scrollView.__clientHeight);
              } :
              function () {
                return Math.floor(decimalValue * scrollView.__clientWidth);
              };
          } else {
            // For static constants, just store the static constant.
            dimensionData.value = parseInt(parsedValue());
          }

        } else {
          dimensionData.dynamic = true;
          dimensionData.getValue = dimensionData === heightData ?
            function heightGetter(scope, locals) {
              var result = parsedValue(scope, locals);
              if (result.charAt && result.charAt(result.length - 1) === '%') {
                return Math.floor(parseFloat(result) / 100 * scrollView.__clientHeight);
              }
              return parseInt(result);
            } :
            function widthGetter(scope, locals) {
              var result = parsedValue(scope, locals);
              if (result.charAt && result.charAt(result.length - 1) === '%') {
                return Math.floor(parseFloat(result) / 100 * scrollView.__clientWidth);
              }
              return parseInt(result);
            };
        }
      }

      var computedStyleNode;
      var computedStyleScope;

      function computeStyleDimensions() {
        if (!computedStyleNode) {
          transclude(computedStyleScope = scope.$new(), function (clone) {
            clone[0].removeAttribute('collection-repeat'); // remove absolute position styling
            computedStyleNode = clone[0];
          });
        }

        computedStyleScope[keyExpr] = (listGetter(scope) || [])[0];
        if (!$rootScope.$$phase) computedStyleScope.$digest();
        containerNode.appendChild(computedStyleNode);

        var style = $window.getComputedStyle(computedStyleNode);
        computedStyleDimensions.width = parseInt(style.width);
        computedStyleDimensions.height = parseInt(style.height);

        containerNode.removeChild(computedStyleNode);
      }

    }

  }

  RepeatManagerFactory.$inject = ['$rootScope', '$window', '$$rAF'];

  function RepeatManagerFactory($rootScope, $window, $$rAF) {
    var EMPTY_DIMENSION = {primaryPos: 0, secondaryPos: 0, primarySize: 0, secondarySize: 0, rowPrimarySize: 0};

    return function RepeatController(options) {
      var afterItemsNode = options.afterItemsNode;
      var containerNode = options.containerNode;
      var forceRefreshImages = options.forceRefreshImages;
      var heightData = options.heightData;
      var widthData = options.widthData;
      var keyExpression = options.keyExpression;
      var renderBuffer = options.renderBuffer;
      var scope = options.scope;
      var scrollView = options.scrollView;
      var transclude = options.transclude;

      var data = [];

      var getterLocals = {};
      var heightFn = heightData.getValue || function () {
        return heightData.value;
      };
      var heightGetter = function (index, value) {
        getterLocals[keyExpression] = value;
        getterLocals.$index = index;
        return heightFn(scope, getterLocals);
      };

      var widthFn = widthData.getValue || function () {
        return widthData.value;
      };
      var widthGetter = function (index, value) {
        getterLocals[keyExpression] = value;
        getterLocals.$index = index;
        return widthFn(scope, getterLocals);
      };

      var isVertical = !!scrollView.options.scrollingY;

      // We say it's a grid view if we're either dynamic or not 100% width
      var isGridView = isVertical ?
        (widthData.dynamic || widthData.value !== scrollView.__clientWidth) :
        (heightData.dynamic || heightData.value !== scrollView.__clientHeight);

      var isStaticView = !heightData.dynamic && !widthData.dynamic;

      var PRIMARY = 'PRIMARY';
      var SECONDARY = 'SECONDARY';
      var TRANSLATE_TEMPLATE_STR = isVertical ?
        'translate3d(SECONDARYpx,PRIMARYpx,0)' :
        'translate3d(PRIMARYpx,SECONDARYpx,0)';
      var WIDTH_HEIGHT_TEMPLATE_STR = isVertical ?
        'height: PRIMARYpx; width: SECONDARYpx;' :
        'height: SECONDARYpx; width: PRIMARYpx;';

      var estimatedHeight;
      var estimatedWidth;

      var repeaterBeforeSize = 0;
      var repeaterAfterSize = 0;

      var renderStartIndex = -1;
      var renderEndIndex = -1;
      var renderAfterBoundary = -1;
      var renderBeforeBoundary = -1;

      var itemsPool = [];
      var itemsLeaving = [];
      var itemsEntering = [];
      var itemsShownMap = {};
      var nextItemId = 0;

      var scrollViewSetDimensions = isVertical ?
        function () {
          scrollView.setDimensions(null, null, null, view.getContentSize(), true);
        } :
        function () {
          scrollView.setDimensions(null, null, view.getContentSize(), null, true);
        };

      // view is a mix of list/grid methods + static/dynamic methods.
      // See bottom for implementations. Available methods:
      //
      // getEstimatedPrimaryPos(i), getEstimatedSecondaryPos(i), getEstimatedIndex(scrollTop),
      // calculateDimensions(toIndex), getDimensions(index),
      // updateRenderRange(scrollTop, scrollValueEnd), onRefreshLayout(), onRefreshData()
      var view = isVertical ? new VerticalViewType() : new HorizontalViewType();
      (isGridView ? GridViewType : ListViewType).call(view);
      (isStaticView ? StaticViewType : DynamicViewType).call(view);

      var contentSizeStr = isVertical ? 'getContentHeight' : 'getContentWidth';
      var originalGetContentSize = scrollView.options[contentSizeStr];
      scrollView.options[contentSizeStr] = angular.bind(view, view.getContentSize);

      scrollView.__$callback = scrollView.__callback;
      scrollView.__callback = function (transformLeft, transformTop, zoom, wasResize) {
        var scrollValue = view.getScrollValue();
        if (renderStartIndex === -1 ||
          scrollValue + view.scrollPrimarySize > renderAfterBoundary ||
          scrollValue < renderBeforeBoundary) {
          render();
        }
        scrollView.__$callback(transformLeft, transformTop, zoom, wasResize);
      };

      var isLayoutReady = false;
      var isDataReady = false;
      this.refreshLayout = function () {
        if (data.length) {
          estimatedHeight = heightGetter(0, data[0]);
          estimatedWidth = widthGetter(0, data[0]);
        } else {
          // If we don't have any data in our array, just guess.
          estimatedHeight = 100;
          estimatedWidth = 100;
        }

        // Get the size of every element AFTER the repeater. We have to get the margin before and
        // after the first/last element to fix a browser bug with getComputedStyle() not counting
        // the first/last child's margins into height.
        var style = getComputedStyle(afterItemsNode) || {};
        var firstStyle = afterItemsNode.firstElementChild && getComputedStyle(afterItemsNode.firstElementChild) || {};
        var lastStyle = afterItemsNode.lastElementChild && getComputedStyle(afterItemsNode.lastElementChild) || {};
        repeaterAfterSize = (parseInt(style[isVertical ? 'height' : 'width']) || 0) +
          (firstStyle && parseInt(firstStyle[isVertical ? 'marginTop' : 'marginLeft']) || 0) +
          (lastStyle && parseInt(lastStyle[isVertical ? 'marginBottom' : 'marginRight']) || 0);

        // Get the offsetTop of the repeater.
        repeaterBeforeSize = 0;
        var current = containerNode;
        do {
          repeaterBeforeSize += current[isVertical ? 'offsetTop' : 'offsetLeft'];
        } while (ionic.DomUtil.contains(scrollView.__content, current = current.offsetParent));

        var containerPrevNode = containerNode.previousElementSibling;
        var beforeStyle = containerPrevNode ? $window.getComputedStyle(containerPrevNode) : {};
        var beforeMargin = parseInt(beforeStyle[isVertical ? 'marginBottom' : 'marginRight'] || 0);

        // Because we position the collection container with position: relative, it doesn't take
        // into account where to position itself relative to the previous element's marginBottom.
        // To compensate, we translate the container up by the previous element's margin.
        containerNode.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR
        .replace(PRIMARY, -beforeMargin)
        .replace(SECONDARY, 0);
        repeaterBeforeSize -= beforeMargin;

        if (!scrollView.__clientHeight || !scrollView.__clientWidth) {
          scrollView.__clientWidth = scrollView.__container.clientWidth;
          scrollView.__clientHeight = scrollView.__container.clientHeight;
        }

        (view.onRefreshLayout || angular.noop)();
        view.refreshDirection();
        scrollViewSetDimensions();

        // Create the pool of items for reuse, setting the size to (estimatedItemsOnScreen) * 2,
        // plus the size of the renderBuffer.
        if (!isLayoutReady) {
          var poolSize = Math.max(20, renderBuffer * 3);
          for (var i = 0; i < poolSize; i++) {
            itemsPool.push(new RepeatItem());
          }
        }

        isLayoutReady = true;
        if (isLayoutReady && isDataReady) {
          // If the resize or latest data change caused the scrollValue to
          // now be out of bounds, resize the scrollView.
          if (scrollView.__scrollLeft > scrollView.__maxScrollLeft ||
            scrollView.__scrollTop > scrollView.__maxScrollTop) {
            scrollView.resize();
          }
          forceRerender(true);
        }
      };

      this.setData = function (newData) {
        data = newData;
        (view.onRefreshData || angular.noop)();
        isDataReady = true;
      };

      this.destroy = function () {
        render.destroyed = true;

        itemsPool.forEach(function (item) {
          item.scope.$destroy();
          item.scope = item.element = item.node = item.images = null;
        });
        itemsPool.length = itemsEntering.length = itemsLeaving.length = 0;
        itemsShownMap = {};

        //Restore the scrollView's normal behavior and resize it to normal size.
        scrollView.options[contentSizeStr] = originalGetContentSize;
        scrollView.__callback = scrollView.__$callback;
        scrollView.resize();

        (view.onDestroy || angular.noop)();
      };

      function forceRerender() {
        return render(true);
      }

      function render(forceRerender) {
        if (render.destroyed) return;
        var i;
        var ii;
        var item;
        var dim;
        var scope;
        var scrollValue = view.getScrollValue();
        var scrollValueEnd = scrollValue + view.scrollPrimarySize;

        view.updateRenderRange(scrollValue, scrollValueEnd);

        renderStartIndex = Math.max(0, renderStartIndex - renderBuffer);
        renderEndIndex = Math.min(data.length - 1, renderEndIndex + renderBuffer);

        for (i in itemsShownMap) {
          if (i < renderStartIndex || i > renderEndIndex) {
            item = itemsShownMap[i];
            delete itemsShownMap[i];
            itemsLeaving.push(item);
            item.isShown = false;
          }
        }

        // Render indicies that aren't shown yet
        //
        // NOTE(ajoslin): this may sound crazy, but calling any other functions during this render
        // loop will often push the render time over the edge from less than one frame to over
        // one frame, causing visible jank.
        // DON'T call any other functions inside this loop unless it's vital.
        for (i = renderStartIndex; i <= renderEndIndex; i++) {
          // We only go forward with render if the index is in data, the item isn't already shown,
          // or forceRerender is on.
          if (i >= data.length || (itemsShownMap[i] && !forceRerender)) continue;

          item = itemsShownMap[i] || (itemsShownMap[i] = itemsLeaving.length ? itemsLeaving.pop() :
            itemsPool.length ? itemsPool.shift() :
              new RepeatItem());
          itemsEntering.push(item);
          item.isShown = true;

          scope = item.scope;
          scope.$index = i;
          scope[keyExpression] = data[i];
          scope.$first = (i === 0);
          scope.$last = (i === (data.length - 1));
          scope.$middle = !(scope.$first || scope.$last);
          scope.$odd = !(scope.$even = (i & 1) === 0);

          if (scope.$$disconnected) ionic.Utils.reconnectScope(item.scope);

          dim = view.getDimensions(i);
          if (item.secondaryPos !== dim.secondaryPos || item.primaryPos !== dim.primaryPos) {
            item.node.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR
            .replace(PRIMARY, (item.primaryPos = dim.primaryPos))
            .replace(SECONDARY, (item.secondaryPos = dim.secondaryPos));
          }
          if (item.secondarySize !== dim.secondarySize || item.primarySize !== dim.primarySize) {
            item.node.style.cssText = item.node.style.cssText
            .replace(WIDTH_HEIGHT_REGEX, WIDTH_HEIGHT_TEMPLATE_STR
              //TODO fix item.primarySize + 1 hack
              .replace(PRIMARY, (item.primarySize = dim.primarySize) + 1)
              .replace(SECONDARY, (item.secondarySize = dim.secondarySize))
            );
          }

        }

        // If we reach the end of the list, render the afterItemsNode - this contains all the
        // elements the developer placed after the collection-repeat
        if (renderEndIndex === data.length - 1) {
          dim = view.getDimensions(data.length - 1) || EMPTY_DIMENSION;
          afterItemsNode.style[ionic.CSS.TRANSFORM] = TRANSLATE_TEMPLATE_STR
          .replace(PRIMARY, dim.primaryPos + dim.primarySize)
          .replace(SECONDARY, 0);
        }

        while (itemsLeaving.length) {
          item = itemsLeaving.pop();
          item.scope.$broadcast('$collectionRepeatLeave');
          ionic.Utils.disconnectScope(item.scope);
          itemsPool.push(item);
          item.node.style[ionic.CSS.TRANSFORM] = 'translate3d(-9999px,-9999px,0)';
          item.primaryPos = item.secondaryPos = null;
        }

        if (forceRefreshImages) {
          for (i = 0, ii = itemsEntering.length; i < ii && (item = itemsEntering[i]); i++) {
            if (!item.images) continue;
            for (var j = 0, jj = item.images.length, img; j < jj && (img = item.images[j]); j++) {
              var src = img.src;
              img.src = ONE_PX_TRANSPARENT_IMG_SRC;
              img.src = src;
            }
          }
        }
        if (forceRerender) {
          var rootScopePhase = $rootScope.$$phase;
          while (itemsEntering.length) {
            item = itemsEntering.pop();
            if (!rootScopePhase) item.scope.$digest();
          }
        } else {
          digestEnteringItems();
        }
      }

      function digestEnteringItems() {
        var item;
        if (digestEnteringItems.running) return;
        digestEnteringItems.running = true;

        $$rAF(function process() {
          var rootScopePhase = $rootScope.$$phase;
          while (itemsEntering.length) {
            item = itemsEntering.pop();
            if (item.isShown) {
              if (!rootScopePhase) item.scope.$digest();
            }
          }
          digestEnteringItems.running = false;
        });
      }

      function RepeatItem() {
        var self = this;
        this.scope = scope.$new();
        this.id = 'item' + (nextItemId++);
        transclude(this.scope, function (clone) {
          self.element = clone;
          self.element.data('$$collectionRepeatItem', self);
          // TODO destroy
          self.node = clone[0];
          // Batch style setting to lower repaints
          self.node.style[ionic.CSS.TRANSFORM] = 'translate3d(-9999px,-9999px,0)';
          self.node.style.cssText += ' height: 0px; width: 0px;';
          ionic.Utils.disconnectScope(self.scope);
          containerNode.appendChild(self.node);
          self.images = clone[0].getElementsByTagName('img');
        });
      }

      function VerticalViewType() {
        this.getItemPrimarySize = heightGetter;
        this.getItemSecondarySize = widthGetter;

        this.getScrollValue = function () {
          return Math.max(0, Math.min(scrollView.__scrollTop - repeaterBeforeSize,
            scrollView.__maxScrollTop - repeaterBeforeSize - repeaterAfterSize));
        };

        this.refreshDirection = function () {
          this.scrollPrimarySize = scrollView.__clientHeight;
          this.scrollSecondarySize = scrollView.__clientWidth;

          this.estimatedPrimarySize = estimatedHeight;
          this.estimatedSecondarySize = estimatedWidth;
          this.estimatedItemsAcross = isGridView &&
            Math.floor(scrollView.__clientWidth / estimatedWidth) ||
            1;
        };
      }

      function HorizontalViewType() {
        this.getItemPrimarySize = widthGetter;
        this.getItemSecondarySize = heightGetter;

        this.getScrollValue = function () {
          return Math.max(0, Math.min(scrollView.__scrollLeft - repeaterBeforeSize,
            scrollView.__maxScrollLeft - repeaterBeforeSize - repeaterAfterSize));
        };

        this.refreshDirection = function () {
          this.scrollPrimarySize = scrollView.__clientWidth;
          this.scrollSecondarySize = scrollView.__clientHeight;

          this.estimatedPrimarySize = estimatedWidth;
          this.estimatedSecondarySize = estimatedHeight;
          this.estimatedItemsAcross = isGridView &&
            Math.floor(scrollView.__clientHeight / estimatedHeight) ||
            1;
        };
      }

      function GridViewType() {
        this.getEstimatedSecondaryPos = function (index) {
          return (index % this.estimatedItemsAcross) * this.estimatedSecondarySize;
        };
        this.getEstimatedPrimaryPos = function (index) {
          return Math.floor(index / this.estimatedItemsAcross) * this.estimatedPrimarySize;
        };
        this.getEstimatedIndex = function (scrollValue) {
          return Math.floor(scrollValue / this.estimatedPrimarySize) *
            this.estimatedItemsAcross;
        };
      }

      function ListViewType() {
        this.getEstimatedSecondaryPos = function () {
          return 0;
        };
        this.getEstimatedPrimaryPos = function (index) {
          return index * this.estimatedPrimarySize;
        };
        this.getEstimatedIndex = function (scrollValue) {
          return Math.floor((scrollValue) / this.estimatedPrimarySize);
        };
      }

      function StaticViewType() {
        this.getContentSize = function () {
          return this.getEstimatedPrimaryPos(data.length - 1) + this.estimatedPrimarySize +
            repeaterBeforeSize + repeaterAfterSize;
        };
        // static view always returns the same object for getDimensions, to avoid memory allocation
        // while scrolling. This could be dangerous if this was a public function, but it's not.
        // Only we use it.
        var dim = {};
        this.getDimensions = function (index) {
          dim.primaryPos = this.getEstimatedPrimaryPos(index);
          dim.secondaryPos = this.getEstimatedSecondaryPos(index);
          dim.primarySize = this.estimatedPrimarySize;
          dim.secondarySize = this.estimatedSecondarySize;
          return dim;
        };
        this.updateRenderRange = function (scrollValue, scrollValueEnd) {
          renderStartIndex = Math.max(0, this.getEstimatedIndex(scrollValue));

          // Make sure the renderEndIndex takes into account all the items on the row
          renderEndIndex = Math.min(data.length - 1,
            this.getEstimatedIndex(scrollValueEnd) + this.estimatedItemsAcross - 1);

          renderBeforeBoundary = Math.max(0,
            this.getEstimatedPrimaryPos(renderStartIndex));
          renderAfterBoundary = this.getEstimatedPrimaryPos(renderEndIndex) +
            this.estimatedPrimarySize;
        };
      }

      function DynamicViewType() {
        var self = this;
        var debouncedScrollViewSetDimensions = ionic.debounce(scrollViewSetDimensions, 25, true);
        var calculateDimensions = isGridView ? calculateDimensionsGrid : calculateDimensionsList;
        var dimensionsIndex;
        var dimensions = [];


        // Get the dimensions at index. {width, height, left, top}.
        // We start with no dimensions calculated, then any time dimensions are asked for at an
        // index we calculate dimensions up to there.
        function calculateDimensionsList(toIndex) {
          var i, prevDimension, dim;
          for (i = Math.max(0, dimensionsIndex); i <= toIndex && (dim = dimensions[i]); i++) {
            prevDimension = dimensions[i - 1] || EMPTY_DIMENSION;
            dim.primarySize = self.getItemPrimarySize(i, data[i]);
            dim.secondarySize = self.scrollSecondarySize;
            dim.primaryPos = prevDimension.primaryPos + prevDimension.primarySize;
            dim.secondaryPos = 0;
          }
        }

        function calculateDimensionsGrid(toIndex) {
          var i, prevDimension, dim;
          for (i = Math.max(dimensionsIndex, 0); i <= toIndex && (dim = dimensions[i]); i++) {
            prevDimension = dimensions[i - 1] || EMPTY_DIMENSION;
            dim.secondarySize = Math.min(
              self.getItemSecondarySize(i, data[i]),
              self.scrollSecondarySize
            );
            dim.secondaryPos = prevDimension.secondaryPos + prevDimension.secondarySize;

            if (i === 0 || dim.secondaryPos + dim.secondarySize > self.scrollSecondarySize) {
              dim.secondaryPos = 0;
              dim.primarySize = self.getItemPrimarySize(i, data[i]);
              dim.primaryPos = prevDimension.primaryPos + prevDimension.rowPrimarySize;

              dim.rowStartIndex = i;
              dim.rowPrimarySize = dim.primarySize;
            } else {
              dim.primarySize = self.getItemPrimarySize(i, data[i]);
              dim.primaryPos = prevDimension.primaryPos;
              dim.rowStartIndex = prevDimension.rowStartIndex;

              dimensions[dim.rowStartIndex].rowPrimarySize = dim.rowPrimarySize = Math.max(
                dimensions[dim.rowStartIndex].rowPrimarySize,
                dim.primarySize
              );
              dim.rowPrimarySize = Math.max(dim.primarySize, dim.rowPrimarySize);
            }
          }
        }

        this.getContentSize = function () {
          var dim = dimensions[dimensionsIndex] || EMPTY_DIMENSION;
          return ((dim.primaryPos + dim.primarySize) || 0) +
            this.getEstimatedPrimaryPos(data.length - dimensionsIndex - 1) +
            repeaterBeforeSize + repeaterAfterSize;
        };
        this.onDestroy = function () {
          dimensions.length = 0;
        };

        this.onRefreshData = function () {
          var i;
          var ii;
          // Make sure dimensions has as many items as data.length.
          // This is to be sure we don't have to allocate objects while scrolling.
          for (i = dimensions.length, ii = data.length; i < ii; i++) {
            dimensions.push({});
          }
          dimensionsIndex = -1;
        };
        this.onRefreshLayout = function () {
          dimensionsIndex = -1;
        };
        this.getDimensions = function (index) {
          index = Math.min(index, data.length - 1);

          if (dimensionsIndex < index) {
            // Once we start asking for dimensions near the end of the list, go ahead and calculate
            // everything. This is to make sure when the user gets to the end of the list, the
            // scroll height of the list is 100% accurate (not estimated anymore).
            if (index > data.length * 0.9) {
              calculateDimensions(data.length - 1);
              dimensionsIndex = data.length - 1;
              scrollViewSetDimensions();
            } else {
              calculateDimensions(index);
              dimensionsIndex = index;
              debouncedScrollViewSetDimensions();
            }

          }
          return dimensions[index];
        };

        var oldRenderStartIndex = -1;
        var oldScrollValue = -1;
        this.updateRenderRange = function (scrollValue, scrollValueEnd) {
          var i;
          var len;
          var dim;

          // Calculate more dimensions than we estimate we'll need, to be sure.
          this.getDimensions(this.getEstimatedIndex(scrollValueEnd) * 2);

          // -- Calculate renderStartIndex
          // base case: start at 0
          if (oldRenderStartIndex === -1 || scrollValue === 0) {
            i = 0;
            // scrolling down
          } else if (scrollValue >= oldScrollValue) {
            for (i = oldRenderStartIndex, len = data.length; i < len; i++) {
              if ((dim = this.getDimensions(i)) && dim.primaryPos + dim.rowPrimarySize >= scrollValue) {
                break;
              }
            }
            // scrolling up
          } else {
            for (i = oldRenderStartIndex; i >= 0; i--) {
              if ((dim = this.getDimensions(i)) && dim.primaryPos <= scrollValue) {
                // when grid view, make sure the render starts at the beginning of a row.
                i = isGridView ? dim.rowStartIndex : i;
                break;
              }
            }
          }

          renderStartIndex = Math.min(Math.max(0, i), data.length - 1);
          renderBeforeBoundary = renderStartIndex !== -1 ? this.getDimensions(renderStartIndex).primaryPos : -1;

          // -- Calculate renderEndIndex
          var lastRowDim;
          for (i = renderStartIndex + 1, len = data.length; i < len; i++) {
            if ((dim = this.getDimensions(i)) && dim.primaryPos + dim.rowPrimarySize > scrollValueEnd) {

              // Go all the way to the end of the row if we're in a grid
              if (isGridView) {
                lastRowDim = dim;
                while (i < len - 1 &&
                (dim = this.getDimensions(i + 1)).primaryPos === lastRowDim.primaryPos) {
                  i++;
                }
              }
              break;
            }
          }

          renderEndIndex = Math.min(i, data.length - 1);
          renderAfterBoundary = renderEndIndex !== -1 ?
            ((dim = this.getDimensions(renderEndIndex)).primaryPos + (dim.rowPrimarySize || dim.primarySize)) :
            -1;

          oldScrollValue = scrollValue;
          oldRenderStartIndex = renderStartIndex;
        };
      }


    };

  }

  /**
   * @ngdoc directive
   * @name ionContent
   * @module ionic
   * @delegate ionic.service:$ionicScrollDelegate
   * @restrict E
   *
   * @description
   * The ionContent directive provides an easy to use content area that can be configured
   * to use Ionic's custom Scroll View, or the built in overflow scrolling of the browser.
   *
   * While we recommend using the custom Scroll features in Ionic in most cases, sometimes
   * (for performance reasons) only the browser's native overflow scrolling will suffice,
   * and so we've made it easy to toggle between the Ionic scroll implementation and
   * overflow scrolling.
   *
   * You can implement pull-to-refresh with the {@link ionic.directive:ionRefresher}
   * directive, and infinite scrolling with the {@link ionic.directive:ionInfiniteScroll}
   * directive.
   *
   * If there is any dynamic content inside the ion-content, be sure to call `.resize()` with {@link ionic.service:$ionicScrollDelegate}
   * after the content has been added.
   *
   * Be aware that this directive gets its own child scope. If you do not understand why this
   * is important, you can read [https://docs.angularjs.org/guide/scope](https://docs.angularjs.org/guide/scope).
   *
   * @param {string=} delegate-handle The handle used to identify this scrollView
   * with {@link ionic.service:$ionicScrollDelegate}.
   * @param {string=} direction Which way to scroll. 'x' or 'y' or 'xy'. Default 'y'.
   * @param {boolean=} locking Whether to lock scrolling in one direction at a time. Useful to set to false when zoomed in or scrolling in two directions. Default true.
   * @param {boolean=} padding Whether to add padding to the content.
   * Defaults to true on iOS, false on Android.
   * @param {boolean=} scroll Whether to allow scrolling of content.  Defaults to true.
   * @param {boolean=} overflow-scroll Whether to use overflow-scrolling instead of
   * Ionic scroll. See {@link ionic.provider:$ionicConfigProvider} to set this as the global default.
   * @param {boolean=} scrollbar-x Whether to show the horizontal scrollbar. Default true.
   * @param {boolean=} scrollbar-y Whether to show the vertical scrollbar. Default true.
   * @param {string=} start-x Initial horizontal scroll position. Default 0.
   * @param {string=} start-y Initial vertical scroll position. Default 0.
   * @param {expression=} on-scroll Expression to evaluate when the content is scrolled.
   * @param {expression=} on-scroll-complete Expression to evaluate when a scroll action completes. Has access to 'scrollLeft' and 'scrollTop' locals.
   * @param {boolean=} has-bouncing Whether to allow scrolling to bounce past the edges
   * of the content.  Defaults to true on iOS, false on Android.
   * @param {number=} scroll-event-interval Number of milliseconds between each firing of the 'on-scroll' expression. Default 10.
   */
  IonicModule
  .directive('ionContent', [
    '$timeout',
    '$controller',
    '$ionicBind',
    '$ionicConfig',
    function ($timeout, $controller, $ionicBind, $ionicConfig) {
      return {
        restrict: 'E',
        require: '^?ionNavView',
        scope: true,
        priority: 800,
        compile: function (element, attr) {
          var innerElement;
          var scrollCtrl;

          element.addClass('scroll-content ionic-scroll');

          if (attr.scroll != 'false') {
            //We cannot use normal transclude here because it breaks element.data()
            //inheritance on compile
            innerElement = jqLite('<div class="scroll"></div>');
            innerElement.append(element.contents());
            element.append(innerElement);
          } else {
            element.addClass('scroll-content-false');
          }

          var nativeScrolling = attr.overflowScroll !== "false" && (attr.overflowScroll === "true" || !$ionicConfig.scrolling.jsScrolling());

          // collection-repeat requires JS scrolling
          if (nativeScrolling) {
            nativeScrolling = !element[0].querySelector('[collection-repeat]');
          }
          return {pre: prelink};

          function prelink($scope, $element, $attr) {
            var parentScope = $scope.$parent;
            $scope.$watch(function () {
              return (parentScope.$hasHeader ? ' has-header' : '') +
                (parentScope.$hasSubheader ? ' has-subheader' : '') +
                (parentScope.$hasFooter ? ' has-footer' : '') +
                (parentScope.$hasSubfooter ? ' has-subfooter' : '') +
                (parentScope.$hasTabs ? ' has-tabs' : '') +
                (parentScope.$hasTabsTop ? ' has-tabs-top' : '');
            }, function (className, oldClassName) {
              $element.removeClass(oldClassName);
              $element.addClass(className);
            });

            //Only this ionContent should use these variables from parent scopes
            $scope.$hasHeader = $scope.$hasSubheader =
              $scope.$hasFooter = $scope.$hasSubfooter =
                $scope.$hasTabs = $scope.$hasTabsTop =
                  false;
            $ionicBind($scope, $attr, {
              $onScroll: '&onScroll',
              $onScrollComplete: '&onScrollComplete',
              hasBouncing: '@',
              padding: '@',
              direction: '@',
              scrollbarX: '@',
              scrollbarY: '@',
              startX: '@',
              startY: '@',
              scrollEventInterval: '@'
            });
            $scope.direction = $scope.direction || 'y';

            if (isDefined($attr.padding)) {
              $scope.$watch($attr.padding, function (newVal) {
                (innerElement || $element).toggleClass('padding', !!newVal);
              });
            }

            if ($attr.scroll === "false") {
              //do nothing
            } else {
              var scrollViewOptions = {};

              // determined in compile phase above
              if (nativeScrolling) {
                // use native scrolling
                $element.addClass('overflow-scroll');

                scrollViewOptions = {
                  el: $element[0],
                  delegateHandle: attr.delegateHandle,
                  startX: $scope.$eval($scope.startX) || 0,
                  startY: $scope.$eval($scope.startY) || 0,
                  nativeScrolling: true
                };

              } else {
                // Use JS scrolling
                scrollViewOptions = {
                  el: $element[0],
                  delegateHandle: attr.delegateHandle,
                  locking: (attr.locking || 'true') === 'true',
                  bouncing: $scope.$eval($scope.hasBouncing),
                  startX: $scope.$eval($scope.startX) || 0,
                  startY: $scope.$eval($scope.startY) || 0,
                  scrollbarX: $scope.$eval($scope.scrollbarX) !== false,
                  scrollbarY: $scope.$eval($scope.scrollbarY) !== false,
                  scrollingX: $scope.direction.indexOf('x') >= 0,
                  scrollingY: $scope.direction.indexOf('y') >= 0,
                  scrollEventInterval: parseInt($scope.scrollEventInterval, 10) || 10,
                  scrollingComplete: onScrollComplete
                };
              }

              // init scroll controller with appropriate options
              scrollCtrl = $controller('$ionicScroll', {
                $scope: $scope,
                scrollViewOptions: scrollViewOptions
              });

              $scope.scrollCtrl = scrollCtrl;

              $scope.$on('$destroy', function () {
                if (scrollViewOptions) {
                  scrollViewOptions.scrollingComplete = noop;
                  delete scrollViewOptions.el;
                }
                innerElement = null;
                $element = null;
                attr.$$element = null;
              });
            }

            function onScrollComplete() {
              $scope.$onScrollComplete({
                scrollTop: scrollCtrl.scrollView.__scrollTop,
                scrollLeft: scrollCtrl.scrollView.__scrollLeft
              });
            }

          }
        }
      };
    }]);

  /**
   * @ngdoc directive
   * @name exposeAsideWhen
   * @module ionic
   * @restrict A
   * @parent ionic.directive:ionSideMenus
   *
   * @description
   * It is common for a tablet application to hide a menu when in portrait mode, but to show the
   * same menu on the left side when the tablet is in landscape mode. The `exposeAsideWhen` attribute
   * directive can be used to accomplish a similar interface.
   *
   * By default, side menus are hidden underneath its side menu content, and can be opened by either
   * swiping the content left or right, or toggling a button to show the side menu. However, by adding the
   * `exposeAsideWhen` attribute directive to an {@link ionic.directive:ionSideMenu} element directive,
   * a side menu can be given instructions on "when" the menu should be exposed (always viewable). For
   * example, the `expose-aside-when="large"` attribute will keep the side menu hidden when the viewport's
   * width is less than `768px`, but when the viewport's width is `768px` or greater, the menu will then
   * always be shown and can no longer be opened or closed like it could when it was hidden for smaller
   * viewports.
   *
   * Using `large` as the attribute's value is a shortcut value to `(min-width:768px)` since it is
   * the most common use-case. However, for added flexibility, any valid media query could be added
   * as the value, such as `(min-width:600px)` or even multiple queries such as
   * `(min-width:750px) and (max-width:1200px)`.
   * @usage
   * ```html
   * <ion-side-menus>
   *   <!-- Center content -->
   *   <ion-side-menu-content>
   *   </ion-side-menu-content>
   *
   *   <!-- Left menu -->
   *   <ion-side-menu expose-aside-when="large">
   *   </ion-side-menu>
   * </ion-side-menus>
   * ```
   * For a complete side menu example, see the
   * {@link ionic.directive:ionSideMenus} documentation.
   */

  IonicModule.directive('exposeAsideWhen', ['$window', function ($window) {
    return {
      restrict: 'A',
      require: '^ionSideMenus',
      link: function ($scope, $element, $attr, sideMenuCtrl) {

        var prevInnerWidth = $window.innerWidth;
        var prevInnerHeight = $window.innerHeight;

        ionic.on('resize', function () {
          if (prevInnerWidth === $window.innerWidth && prevInnerHeight === $window.innerHeight) {
            return;
          }
          prevInnerWidth = $window.innerWidth;
          prevInnerHeight = $window.innerHeight;
          onResize();
        }, $window);

        function checkAsideExpose() {
          var mq = $attr.exposeAsideWhen == 'large' ? '(min-width:768px)' : $attr.exposeAsideWhen;
          sideMenuCtrl.exposeAside($window.matchMedia(mq).matches);
          sideMenuCtrl.activeAsideResizing(false);
        }

        function onResize() {
          sideMenuCtrl.activeAsideResizing(true);
          debouncedCheck();
        }

        var debouncedCheck = ionic.debounce(function () {
          $scope.$apply(checkAsideExpose);
        }, 300, false);

        $scope.$evalAsync(checkAsideExpose);
      }
    };
  }]);

  var GESTURE_DIRECTIVES = 'onHold onTap onDoubleTap onTouch onRelease onDragStart onDrag onDragEnd onDragUp onDragRight onDragDown onDragLeft onSwipe onSwipeUp onSwipeRight onSwipeDown onSwipeLeft'.split(' ');

  GESTURE_DIRECTIVES.forEach(function (name) {
    IonicModule.directive(name, gestureDirective(name));
  });


  /**
   * @ngdoc directive
   * @name onHold
   * @module ionic
   * @restrict A
   *
   * @description
   * Touch stays at the same location for 500ms. Similar to long touch events available for AngularJS and jQuery.
   *
   * @usage
   * ```html
   * <button on-hold="onHold()" class="button">Test</button>
   * ```
   */


  /**
   * @ngdoc directive
   * @name onTap
   * @module ionic
   * @restrict A
   *
   * @description
   * Quick touch at a location. If the duration of the touch goes
   * longer than 250ms it is no longer a tap gesture.
   *
   * @usage
   * ```html
   * <button on-tap="onTap()" class="button">Test</button>
   * ```
   */


  /**
   * @ngdoc directive
   * @name onDoubleTap
   * @module ionic
   * @restrict A
   *
   * @description
   * Double tap touch at a location.
   *
   * @usage
   * ```html
   * <button on-double-tap="onDoubleTap()" class="button">Test</button>
   * ```
   */


  /**
   * @ngdoc directive
   * @name onTouch
   * @module ionic
   * @restrict A
   *
   * @description
   * Called immediately when the user first begins a touch. This
   * gesture does not wait for a touchend/mouseup.
   *
   * @usage
   * ```html
   * <button on-touch="onTouch()" class="button">Test</button>
   * ```
   */


  /**
   * @ngdoc directive
   * @name onRelease
   * @module ionic
   * @restrict A
   *
   * @description
   * Called when the user ends a touch.
   *
   * @usage
   * ```html
   * <button on-release="onRelease()" class="button">Test</button>
   * ```
   */

  /**
   * @ngdoc directive
   * @name onDragStart
   * @module ionic
   * @restrict A
   *
   * @description
   * Called when a drag gesture has started.
   *
   * @usage
   * ```html
   * <button on-drag-start="onDragStart()" class="button">Test</button>
   * ```
   */


  /**
   * @ngdoc directive
   * @name onDrag
   * @module ionic
   * @restrict A
   *
   * @description
   * Move with one touch around on the page. Blocking the scrolling when
   * moving left and right is a good practice. When all the drag events are
   * blocking you disable scrolling on that area.
   *
   * @usage
   * ```html
   * <button on-drag="onDrag()" class="button">Test</button>
   * ```
   */

  /**
   * @ngdoc directive
   * @name onDragEnd
   * @module ionic
   * @restrict A
   *
   * @description
   * Called when a drag gesture has ended.
   *
   * @usage
   * ```html
   * <button on-drag-end="onDragEnd()" class="button">Test</button>
   * ```
   */

  /**
   * @ngdoc directive
   * @name onDragUp
   * @module ionic
   * @restrict A
   *
   * @description
   * Called when the element is dragged up.
   *
   * @usage
   * ```html
   * <button on-drag-up="onDragUp()" class="button">Test</button>
   * ```
   */


  /**
   * @ngdoc directive
   * @name onDragRight
   * @module ionic
   * @restrict A
   *
   * @description
   * Called when the element is dragged to the right.
   *
   * @usage
   * ```html
   * <button on-drag-right="onDragRight()" class="button">Test</button>
   * ```
   */


  /**
   * @ngdoc directive
   * @name onDragDown
   * @module ionic
   * @restrict A
   *
   * @description
   * Called when the element is dragged down.
   *
   * @usage
   * ```html
   * <button on-drag-down="onDragDown()" class="button">Test</button>
   * ```
   */


  /**
   * @ngdoc directive
   * @name onDragLeft
   * @module ionic
   * @restrict A
   *
   * @description
   * Called when the element is dragged to the left.
   *
   * @usage
   * ```html
   * <button on-drag-left="onDragLeft()" class="button">Test</button>
   * ```
   */


  /**
   * @ngdoc directive
   * @name onSwipe
   * @module ionic
   * @restrict A
   *
   * @description
   * Called when a moving touch has a high velocity in any direction.
   *
   * @usage
   * ```html
   * <button on-swipe="onSwipe()" class="button">Test</button>
   * ```
   */


  /**
   * @ngdoc directive
   * @name onSwipeUp
   * @module ionic
   * @restrict A
   *
   * @description
   * Called when a moving touch has a high velocity moving up.
   *
   * @usage
   * ```html
   * <button on-swipe-up="onSwipeUp()" class="button">Test</button>
   * ```
   */


  /**
   * @ngdoc directive
   * @name onSwipeRight
   * @module ionic
   * @restrict A
   *
   * @description
   * Called when a moving touch has a high velocity moving to the right.
   *
   * @usage
   * ```html
   * <button on-swipe-right="onSwipeRight()" class="button">Test</button>
   * ```
   */


  /**
   * @ngdoc directive
   * @name onSwipeDown
   * @module ionic
   * @restrict A
   *
   * @description
   * Called when a moving touch has a high velocity moving down.
   *
   * @usage
   * ```html
   * <button on-swipe-down="onSwipeDown()" class="button">Test</button>
   * ```
   */


  /**
   * @ngdoc directive
   * @name onSwipeLeft
   * @module ionic
   * @restrict A
   *
   * @description
   * Called when a moving touch has a high velocity moving to the left.
   *
   * @usage
   * ```html
   * <button on-swipe-left="onSwipeLeft()" class="button">Test</button>
   * ```
   */


  function gestureDirective(directiveName) {
    return ['$ionicGesture', '$parse', function ($ionicGesture, $parse) {
      var eventType = directiveName.substr(2).toLowerCase();

      return function (scope, element, attr) {
        var fn = $parse(attr[directiveName]);

        var listener = function (ev) {
          scope.$apply(function () {
            fn(scope, {
              $event: ev
            });
          });
        };

        var gesture = $ionicGesture.on(eventType, listener, element);

        scope.$on('$destroy', function () {
          $ionicGesture.off(gesture, eventType, listener);
        });
      };
    }];
  }


  IonicModule
  //.directive('ionHeaderBar', tapScrollToTopDirective())

  /**
   * @ngdoc directive
   * @name ionHeaderBar
   * @module ionic
   * @restrict E
   *
   * @description
   * Adds a fixed header bar above some content.
   *
   * Can also be a subheader (lower down) if the 'bar-subheader' class is applied.
   * See [the header CSS docs](/docs/components/#subheader).
   *
   * @param {string=} align-title How to align the title. By default the title
   * will be aligned the same as how the platform aligns its titles (iOS centers
   * titles, Android aligns them left).
   * Available: 'left', 'right', or 'center'.  Defaults to the same as the platform.
   * @param {boolean=} no-tap-scroll By default, the header bar will scroll the
   * content to the top when tapped.  Set no-tap-scroll to true to disable this
   * behavior.
   * Available: true or false.  Defaults to false.
   *
   * @usage
   * ```html
   * <ion-header-bar align-title="left" class="bar-positive">
   *   <div class="buttons">
   *     <button class="button" ng-click="doSomething()">Left Button</button>
   *   </div>
   *   <h1 class="title">Title!</h1>
   *   <div class="buttons">
   *     <button class="button">Right Button</button>
   *   </div>
   * </ion-header-bar>
   * <ion-content class="has-header">
   *   Some content!
   * </ion-content>
   * ```
   */
  .directive('ionHeaderBar', headerFooterBarDirective(true))

  /**
   * @ngdoc directive
   * @name ionFooterBar
   * @module ionic
   * @restrict E
   *
   * @description
   * Adds a fixed footer bar below some content.
   *
   * Can also be a subfooter (higher up) if the 'bar-subfooter' class is applied.
   * See [the footer CSS docs](/docs/components/#footer).
   *
   * Note: If you use ionFooterBar in combination with ng-if, the surrounding content
   * will not align correctly.  This will be fixed soon.
   *
   * @param {string=} align-title Where to align the title.
   * Available: 'left', 'right', or 'center'.  Defaults to 'center'.
   *
   * @usage
   * ```html
   * <ion-content class="has-footer">
   *   Some content!
   * </ion-content>
   * <ion-footer-bar align-title="left" class="bar-assertive">
   *   <div class="buttons">
   *     <button class="button">Left Button</button>
   *   </div>
   *   <h1 class="title">Title!</h1>
   *   <div class="buttons" ng-click="doSomething()">
   *     <button class="button">Right Button</button>
   *   </div>
   * </ion-footer-bar>
   * ```
   */
  .directive('ionFooterBar', headerFooterBarDirective(false));

  function tapScrollToTopDirective() { //eslint-disable-line no-unused-vars
    return ['$ionicScrollDelegate', function ($ionicScrollDelegate) {
      return {
        restrict: 'E',
        link: function ($scope, $element, $attr) {
          if ($attr.noTapScroll == 'true') {
            return;
          }
          ionic.on('tap', onTap, $element[0]);
          $scope.$on('$destroy', function () {
            ionic.off('tap', onTap, $element[0]);
          });

          function onTap(e) {
            var depth = 3;
            var current = e.target;
            //Don't scroll to top in certain cases
            while (depth-- && current) {
              if (current.classList.contains('button') ||
                current.tagName.match(/input|textarea|select/i) ||
                current.isContentEditable) {
                return;
              }
              current = current.parentNode;
            }
            var touch = e.gesture && e.gesture.touches[0] || e.detail.touches[0];
            var bounds = $element[0].getBoundingClientRect();
            if (ionic.DomUtil.rectContains(
              touch.pageX, touch.pageY,
              bounds.left, bounds.top - 20,
              bounds.left + bounds.width, bounds.top + bounds.height
            )) {
              $ionicScrollDelegate.scrollTop(true);
            }
          }
        }
      };
    }];
  }

  function headerFooterBarDirective(isHeader) {
    return ['$document', '$timeout', function ($document, $timeout) {
      return {
        restrict: 'E',
        controller: '$ionicHeaderBar',
        compile: function (tElement) {
          tElement.addClass(isHeader ? 'bar bar-header' : 'bar bar-footer');
          // top style tabs? if so, remove bottom border for seamless display
          $timeout(function () {
            if (isHeader && $document[0].getElementsByClassName('tabs-top').length) tElement.addClass('has-tabs-top');
          });

          return {pre: prelink};

          function prelink($scope, $element, $attr, ctrl) {
            if (isHeader) {
              $scope.$watch(function () {
                return $element[0].className;
              }, function (value) {
                var isShown = value.indexOf('ng-hide') === -1;
                var isSubheader = value.indexOf('bar-subheader') !== -1;
                $scope.$hasHeader = isShown && !isSubheader;
                $scope.$hasSubheader = isShown && isSubheader;
                $scope.$emit('$ionicSubheader', $scope.$hasSubheader);
              });
              $scope.$on('$destroy', function () {
                delete $scope.$hasHeader;
                delete $scope.$hasSubheader;
              });
              ctrl.align();
              $scope.$on('$ionicHeader.align', function () {
                ionic.requestAnimationFrame(function () {
                  ctrl.align();
                });
              });

            } else {
              $scope.$watch(function () {
                return $element[0].className;
              }, function (value) {
                var isShown = value.indexOf('ng-hide') === -1;
                var isSubfooter = value.indexOf('bar-subfooter') !== -1;
                $scope.$hasFooter = isShown && !isSubfooter;
                $scope.$hasSubfooter = isShown && isSubfooter;
              });
              $scope.$on('$destroy', function () {
                delete $scope.$hasFooter;
                delete $scope.$hasSubfooter;
              });
              $scope.$watch('$hasTabs', function (val) {
                $element.toggleClass('has-tabs', !!val);
              });
              ctrl.align();
              $scope.$on('$ionicFooter.align', function () {
                ionic.requestAnimationFrame(function () {
                  ctrl.align();
                });
              });
            }
          }
        }
      };
    }];
  }

  /**
   * @ngdoc directive
   * @name ionInfiniteScroll
   * @module ionic
   * @parent ionic.directive:ionContent, ionic.directive:ionScroll
   * @restrict E
   *
   * @description
   * The ionInfiniteScroll directive allows you to call a function whenever
   * the user gets to the bottom of the page or near the bottom of the page.
   *
   * The expression you pass in for `on-infinite` is called when the user scrolls
   * greater than `distance` away from the bottom of the content.  Once `on-infinite`
   * is done loading new data, it should broadcast the `scroll.infiniteScrollComplete`
   * event from your controller (see below example).
   *
   * @param {expression} on-infinite What to call when the scroller reaches the
   * bottom.
   * @param {string=} distance The distance from the bottom that the scroll must
   * reach to trigger the on-infinite expression. Default: 1%.
   * @param {string=} spinner The {@link ionic.directive:ionSpinner} to show while loading. The SVG
   * {@link ionic.directive:ionSpinner} is now the default, replacing rotating font icons.
   * @param {string=} icon The icon to show while loading. Default: 'ion-load-d'.  This is depreciated
   * in favor of the SVG {@link ionic.directive:ionSpinner}.
   * @param {boolean=} immediate-check Whether to check the infinite scroll bounds immediately on load.
   *
   * @usage
   * ```html
   * <ion-content ng-controller="MyController">
   *   <ion-list>
   *   ....
   *   ....
   *   </ion-list>
   *
   *   <ion-infinite-scroll
   *     on-infinite="loadMore()"
   *     distance="1%">
   *   </ion-infinite-scroll>
   * </ion-content>
   * ```
   * ```js
   * function MyController($scope, $http) {
   *   $scope.items = [];
   *   $scope.loadMore = function() {
   *     $http.get('/more-items').success(function(items) {
   *       useItems(items);
   *       $scope.$broadcast('scroll.infiniteScrollComplete');
   *     });
   *   };
   *
   *   $scope.$on('$stateChangeSuccess', function() {
   *     $scope.loadMore();
   *   });
   * }
   * ```
   *
   * An easy to way to stop infinite scroll once there is no more data to load
   * is to use angular's `ng-if` directive:
   *
   * ```html
   * <ion-infinite-scroll
   *   ng-if="moreDataCanBeLoaded()"
   *   icon="ion-loading-c"
   *   on-infinite="loadMoreData()">
   * </ion-infinite-scroll>
   * ```
   */
  IonicModule
  .directive('ionInfiniteScroll', ['$timeout', function ($timeout) {
    return {
      restrict: 'E',
      require: ['?^$ionicScroll', 'ionInfiniteScroll'],
      template: function ($element, $attrs) {
        if ($attrs.icon) return '<i class="icon {{icon()}} icon-refreshing {{scrollingType}}"></i>';
        return '<ion-spinner icon="{{spinner()}}"></ion-spinner>';
      },
      scope: true,
      controller: '$ionInfiniteScroll',
      link: function ($scope, $element, $attrs, ctrls) {
        var infiniteScrollCtrl = ctrls[1];
        var scrollCtrl = infiniteScrollCtrl.scrollCtrl = ctrls[0];
        var jsScrolling = infiniteScrollCtrl.jsScrolling = !scrollCtrl.isNative();

        // if this view is not beneath a scrollCtrl, it can't be injected, proceed w/ native scrolling
        if (jsScrolling) {
          infiniteScrollCtrl.scrollView = scrollCtrl.scrollView;
          $scope.scrollingType = 'js-scrolling';
          //bind to JS scroll events
          scrollCtrl.$element.on('scroll', infiniteScrollCtrl.checkBounds);
        } else {
          // grabbing the scrollable element, to determine dimensions, and current scroll pos
          var scrollEl = ionic.DomUtil.getParentOrSelfWithClass($element[0].parentNode, 'overflow-scroll');
          infiniteScrollCtrl.scrollEl = scrollEl;
          // if there's no scroll controller, and no overflow scroll div, infinite scroll wont work
          if (!scrollEl) {
            throw 'Infinite scroll must be used inside a scrollable div';
          }
          //bind to native scroll events
          infiniteScrollCtrl.scrollEl.addEventListener('scroll', infiniteScrollCtrl.checkBounds);
        }

        // Optionally check bounds on start after scrollView is fully rendered
        var doImmediateCheck = isDefined($attrs.immediateCheck) ? $scope.$eval($attrs.immediateCheck) : true;
        if (doImmediateCheck) {
          $timeout(function () {
            infiniteScrollCtrl.checkBounds();
          });
        }
      }
    };
  }]);

  /**
   * @ngdoc directive
   * @name ionInput
   * @parent ionic.directive:ionList
   * @module ionic
   * @restrict E
   * Creates a text input group that can easily be focused
   *
   * @usage
   *
   * ```html
   * <ion-list>
   *   <ion-input>
   *     <input type="text" placeholder="First Name">
   *   </ion-input>
   *
   *   <ion-input>
   *     <ion-label>Username</ion-label>
   *     <input type="text">
   *   </ion-input>
   * </ion-list>
   * ```
   */

  var labelIds = -1;

  IonicModule
  .directive('ionInput', [function () {
    return {
      restrict: 'E',
      controller: ['$scope', '$element', function ($scope, $element) {
        this.$scope = $scope;
        this.$element = $element;

        this.setInputAriaLabeledBy = function (id) {
          var inputs = $element[0].querySelectorAll('input,textarea');
          inputs.length && inputs[0].setAttribute('aria-labelledby', id);
        };

        this.focus = function () {
          var inputs = $element[0].querySelectorAll('input,textarea');
          inputs.length && inputs[0].focus();
        };
      }]
    };
  }]);

  /**
   * @ngdoc directive
   * @name ionLabel
   * @parent ionic.directive:ionList
   * @module ionic
   * @restrict E
   *
   * New in Ionic 1.2. It is strongly recommended that you use `<ion-label>` in place
   * of any `<label>` elements for maximum cross-browser support and performance.
   *
   * Creates a label for a form input.
   *
   * @usage
   *
   * ```html
   * <ion-list>
   *   <ion-input>
   *     <ion-label>Username</ion-label>
   *     <input type="text">
   *   </ion-input>
   * </ion-list>
   * ```
   */
  IonicModule
  .directive('ionLabel', [function () {
    return {
      restrict: 'E',
      require: '?^ionInput',
      compile: function () {

        return function link($scope, $element, $attrs, ionInputCtrl) {
          var element = $element[0];

          $element.addClass('input-label');

          $element.attr('aria-label', $element.text());
          var id = element.id || '_label-' + ++labelIds;

          if (!element.id) {
            $element.attr('id', id);
          }

          if (ionInputCtrl) {

            ionInputCtrl.setInputAriaLabeledBy(id);

            $element.on('click', function () {
              ionInputCtrl.focus();
            });
          }
        };
      }
    };
  }]);

  /**
   * Input label adds accessibility to <span class="input-label">.
   */
  IonicModule
  .directive('inputLabel', [function () {
    return {
      restrict: 'C',
      require: '?^ionInput',
      compile: function () {

        return function link($scope, $element, $attrs, ionInputCtrl) {
          var element = $element[0];

          $element.attr('aria-label', $element.text());
          var id = element.id || '_label-' + ++labelIds;

          if (!element.id) {
            $element.attr('id', id);
          }

          if (ionInputCtrl) {
            ionInputCtrl.setInputAriaLabeledBy(id);
          }

        };
      }
    };
  }]);

  /**
   * @ngdoc directive
   * @name ionItem
   * @parent ionic.directive:ionList
   * @module ionic
   * @restrict E
   * Creates a list-item that can easily be swiped,
   * deleted, reordered, edited, and more.
   *
   * See {@link ionic.directive:ionList} for a complete example & explanation.
   *
   * Can be assigned any item class name. See the
   * [list CSS documentation](/docs/components/#list).
   *
   * @usage
   *
   * ```html
   * <ion-list>
   *   <ion-item>Hello!</ion-item>
   *   <ion-item href="#/detail">
   *     Link to detail page
   *   </ion-item>
   * </ion-list>
   * ```
   */
  IonicModule
  .directive('ionItem', ['$$rAF', function ($$rAF) {
    return {
      restrict: 'E',
      controller: ['$scope', '$element', function ($scope, $element) {
        this.$scope = $scope;
        this.$element = $element;
      }],
      scope: true,
      compile: function ($element, $attrs) {
        var isAnchor = isDefined($attrs.href) ||
          isDefined($attrs.ngHref) ||
          isDefined($attrs.uiSref);
        var isComplexItem = isAnchor ||
          //Lame way of testing, but we have to know at compile what to do with the element
          /ion-(delete|option|reorder)-button/i.test($element.html());

        if (isComplexItem) {
          var innerElement = jqLite(isAnchor ? '<a></a>' : '<div></div>');
          innerElement.addClass('item-content');

          if (isDefined($attrs.href) || isDefined($attrs.ngHref)) {
            innerElement.attr('ng-href', '{{$href()}}');
            if (isDefined($attrs.target)) {
              innerElement.attr('target', '{{$target()}}');
            }
          }

          innerElement.append($element.contents());

          $element.addClass('item item-complex')
          .append(innerElement);
        } else {
          $element.addClass('item');
        }

        return function link($scope, $element, $attrs) {
          $scope.$href = function () {
            return $attrs.href || $attrs.ngHref;
          };
          $scope.$target = function () {
            return $attrs.target;
          };

          var content = $element[0].querySelector('.item-content');
          if (content) {
            $scope.$on('$collectionRepeatLeave', function () {
              if (content && content.$$ionicOptionsOpen) {
                content.style[ionic.CSS.TRANSFORM] = '';
                content.style[ionic.CSS.TRANSITION] = 'none';
                $$rAF(function () {
                  content.style[ionic.CSS.TRANSITION] = '';
                });
                content.$$ionicOptionsOpen = false;
              }
            });
          }
        };

      }
    };
  }]);

  var ITEM_TPL_DELETE_BUTTON =
    '<div class="item-left-edit item-delete enable-pointer-events">' +
    '</div>';
  /**
   * @ngdoc directive
   * @name ionDeleteButton
   * @parent ionic.directive:ionItem
   * @module ionic
   * @restrict E
   * Creates a delete button inside a list item, that is visible when the
   * {@link ionic.directive:ionList ionList parent's} `show-delete` evaluates to true or
   * `$ionicListDelegate.showDelete(true)` is called.
   *
   * Takes any ionicon as a class.
   *
   * See {@link ionic.directive:ionList} for a complete example & explanation.
   *
   * @usage
   *
   * ```html
   * <ion-list show-delete="shouldShowDelete">
   *   <ion-item>
   *     <ion-delete-button class="ion-minus-circled"></ion-delete-button>
   *     Hello, list item!
   *   </ion-item>
   * </ion-list>
   * <ion-toggle ng-model="shouldShowDelete">
   *   Show Delete?
   * </ion-toggle>
   * ```
   */
  IonicModule
  .directive('ionDeleteButton', function () {

    function stopPropagation(ev) {
      ev.stopPropagation();
    }

    return {
      restrict: 'E',
      require: ['^^ionItem', '^?ionList'],
      //Run before anything else, so we can move it before other directives process
      //its location (eg ngIf relies on the location of the directive in the dom)
      priority: Number.MAX_VALUE,
      compile: function ($element, $attr) {
        //Add the classes we need during the compile phase, so that they stay
        //even if something else like ngIf removes the element and re-addss it
        $attr.$set('class', ($attr['class'] || '') + ' button icon button-icon', true);
        return function ($scope, $element, $attr, ctrls) {
          var itemCtrl = ctrls[0];
          var listCtrl = ctrls[1];
          var container = jqLite(ITEM_TPL_DELETE_BUTTON);
          container.append($element);
          itemCtrl.$element.append(container).addClass('item-left-editable');

          //Don't bubble click up to main .item
          $element.on('click', stopPropagation);

          init();
          $scope.$on('$ionic.reconnectScope', init);

          function init() {
            listCtrl = listCtrl || $element.controller('ionList');
            if (listCtrl && listCtrl.showDelete()) {
              container.addClass('visible active');
            }
          }
        };
      }
    };
  });


  IonicModule
  .directive('itemFloatingLabel', function () {
    return {
      restrict: 'C',
      link: function (scope, element) {
        var el = element[0];
        var input = el.querySelector('input, textarea');
        var inputLabel = el.querySelector('.input-label');

        if (!input || !inputLabel) return;

        var onInput = function () {
          if (input.value) {
            inputLabel.classList.add('has-input');
          } else {
            inputLabel.classList.remove('has-input');
          }
        };

        input.addEventListener('input', onInput);

        var ngModelCtrl = jqLite(input).controller('ngModel');
        if (ngModelCtrl) {
          ngModelCtrl.$render = function () {
            input.value = ngModelCtrl.$viewValue || '';
            onInput();
          };
        }

        scope.$on('$destroy', function () {
          input.removeEventListener('input', onInput);
        });
      }
    };
  });

  var ITEM_TPL_OPTION_BUTTONS =
    '<div class="item-options invisible">' +
    '</div>';
  /**
   * @ngdoc directive
   * @name ionOptionButton
   * @parent ionic.directive:ionItem
   * @module ionic
   * @restrict E
   * @description
   * Creates an option button inside a list item, that is visible when the item is swiped
   * to the left by the user.  Swiped open option buttons can be hidden with
   * {@link ionic.service:$ionicListDelegate#closeOptionButtons $ionicListDelegate.closeOptionButtons}.
   *
   * Can be assigned any button class.
   *
   * See {@link ionic.directive:ionList} for a complete example & explanation.
   *
   * @usage
   *
   * ```html
   * <ion-list>
   *   <ion-item>
   *     I love kittens!
   *     <ion-option-button class="button-positive">Share</ion-option-button>
   *     <ion-option-button class="button-assertive">Edit</ion-option-button>
   *   </ion-item>
   * </ion-list>
   * ```
   */
  IonicModule.directive('ionOptionButton', [function () {
    function stopPropagation(e) {
      e.stopPropagation();
    }

    return {
      restrict: 'E',
      require: '^ionItem',
      priority: Number.MAX_VALUE,
      compile: function ($element, $attr) {
        $attr.$set('class', ($attr['class'] || '') + ' button', true);
        return function ($scope, $element, $attr, itemCtrl) {
          if (!itemCtrl.optionsContainer) {
            itemCtrl.optionsContainer = jqLite(ITEM_TPL_OPTION_BUTTONS);
            itemCtrl.$element.prepend(itemCtrl.optionsContainer);
          }
          itemCtrl.optionsContainer.prepend($element);

          itemCtrl.$element.addClass('item-right-editable');

          //Don't bubble click up to main .item
          $element.on('click', stopPropagation);
        };
      }
    };
  }]);

  var ITEM_TPL_REORDER_BUTTON =
    '<div data-prevent-scroll="true" class="item-right-edit item-reorder enable-pointer-events">' +
    '</div>';

  /**
   * @ngdoc directive
   * @name ionReorderButton
   * @parent ionic.directive:ionItem
   * @module ionic
   * @restrict E
   * Creates a reorder button inside a list item, that is visible when the
   * {@link ionic.directive:ionList ionList parent's} `show-reorder` evaluates to true or
   * `$ionicListDelegate.showReorder(true)` is called.
   *
   * Can be dragged to reorder items in the list. Takes any ionicon class.
   *
   * Note: Reordering works best when used with `ng-repeat`.  Be sure that all `ion-item` children of an `ion-list` are part of the same `ng-repeat` expression.
   *
   * When an item reorder is complete, the expression given in the `on-reorder` attribute is called. The `on-reorder` expression is given two locals that can be used: `$fromIndex` and `$toIndex`.  See below for an example.
   *
   * Look at {@link ionic.directive:ionList} for more examples.
   *
   * @usage
   *
   * ```html
   * <ion-list ng-controller="MyCtrl" show-reorder="true">
   *   <ion-item ng-repeat="item in items">
   *     Item {{item}}
   *     <ion-reorder-button class="ion-navicon"
   *                         on-reorder="moveItem(item, $fromIndex, $toIndex)">
   *     </ion-reorder-button>
   *   </ion-item>
   * </ion-list>
   * ```
   * ```js
   * function MyCtrl($scope) {
   *   $scope.items = [1, 2, 3, 4];
   *   $scope.moveItem = function(item, fromIndex, toIndex) {
   *     //Move the item in the array
   *     $scope.items.splice(fromIndex, 1);
   *     $scope.items.splice(toIndex, 0, item);
   *   };
   * }
   * ```
   *
   * @param {expression=} on-reorder Expression to call when an item is reordered.
   * Parameters given: $fromIndex, $toIndex.
   */
  IonicModule
  .directive('ionReorderButton', ['$parse', function ($parse) {
    return {
      restrict: 'E',
      require: ['^ionItem', '^?ionList'],
      priority: Number.MAX_VALUE,
      compile: function ($element, $attr) {
        $attr.$set('class', ($attr['class'] || '') + ' button icon button-icon', true);
        $element[0].setAttribute('data-prevent-scroll', true);
        return function ($scope, $element, $attr, ctrls) {
          var itemCtrl = ctrls[0];
          var listCtrl = ctrls[1];
          var onReorderFn = $parse($attr.onReorder);

          $scope.$onReorder = function (oldIndex, newIndex) {
            onReorderFn($scope, {
              $fromIndex: oldIndex,
              $toIndex: newIndex
            });
          };

          // prevent clicks from bubbling up to the item
          if (!$attr.ngClick && !$attr.onClick && !$attr.onclick) {
            $element[0].onclick = function (e) {
              e.stopPropagation();
              return false;
            };
          }

          var container = jqLite(ITEM_TPL_REORDER_BUTTON);
          container.append($element);
          itemCtrl.$element.append(container).addClass('item-right-editable');

          if (listCtrl && listCtrl.showReorder()) {
            container.addClass('visible active');
          }
        };
      }
    };
  }]);

  /**
   * @ngdoc directive
   * @name keyboardAttach
   * @module ionic
   * @restrict A
   *
   * @description
   * keyboard-attach is an attribute directive which will cause an element to float above
   * the keyboard when the keyboard shows. Currently only supports the
   * [ion-footer-bar]({{ page.versionHref }}/api/directive/ionFooterBar/) directive.
   *
   * ### Notes
   * - This directive requires the
   * [Ionic Keyboard Plugin](https://github.com/ionic-team/ionic-plugins-keyboard).
   * - On Android not in fullscreen mode, i.e. you have
   *   `<preference name="Fullscreen" value="false" />` or no preference in your `config.xml` file,
   *   this directive is unnecessary since it is the default behavior.
   * - On iOS, if there is an input in your footer, you will need to set
   *   `cordova.plugins.Keyboard.disableScroll(true)`.
   *
   * @usage
   *
   * ```html
   *  <ion-footer-bar align-title="left" keyboard-attach class="bar-assertive">
   *    <h1 class="title">Title!</h1>
   *  </ion-footer-bar>
   * ```
   */

  IonicModule
  .directive('keyboardAttach', function () {
    return function (scope, element) {
      ionic.on('native.keyboardshow', onShow, window);
      ionic.on('native.keyboardhide', onHide, window);

      //deprecated
      ionic.on('native.showkeyboard', onShow, window);
      ionic.on('native.hidekeyboard', onHide, window);


      var scrollCtrl;

      function onShow(e) {
        if (ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen) {
          return;
        }

        //for testing
        var keyboardHeight = e.keyboardHeight || (e.detail && e.detail.keyboardHeight);
        element.css('bottom', keyboardHeight + "px");
        scrollCtrl = element.controller('$ionicScroll');
        if (scrollCtrl) {
          scrollCtrl.scrollView.__container.style.bottom = keyboardHeight + keyboardAttachGetClientHeight(element[0]) + "px";
        }
      }

      function onHide() {
        if (ionic.Platform.isAndroid() && !ionic.Platform.isFullScreen) {
          return;
        }

        element.css('bottom', '');
        if (scrollCtrl) {
          scrollCtrl.scrollView.__container.style.bottom = '';
        }
      }

      scope.$on('$destroy', function () {
        ionic.off('native.keyboardshow', onShow, window);
        ionic.off('native.keyboardhide', onHide, window);

        //deprecated
        ionic.off('native.showkeyboard', onShow, window);
        ionic.off('native.hidekeyboard', onHide, window);
      });
    };
  });

  function keyboardAttachGetClientHeight(element) {
    return element.clientHeight;
  }

  /**
   * @ngdoc directive
   * @name ionList
   * @module ionic
   * @delegate ionic.service:$ionicListDelegate
   * @codepen JsHjf
   * @restrict E
   * @description
   * The List is a widely used interface element in almost any mobile app, and can include
   * content ranging from basic text all the way to buttons, toggles, icons, and thumbnails.
   *
   * Both the list, which contains items, and the list items themselves can be any HTML
   * element. The containing element requires the `list` class and each list item requires
   * the `item` class.
   *
   * However, using the ionList and ionItem directives make it easy to support various
   * interaction modes such as swipe to edit, drag to reorder, and removing items.
   *
   * Related: {@link ionic.directive:ionItem}, {@link ionic.directive:ionOptionButton}
   * {@link ionic.directive:ionReorderButton}, {@link ionic.directive:ionDeleteButton}, [`list CSS documentation`](/docs/components/#list).
   *
   * @usage
   *
   * Basic Usage:
   *
   * ```html
   * <ion-list>
   *   <ion-item ng-repeat="item in items">
   *     {% raw %}Hello, {{item}}!{% endraw %}
   *   </ion-item>
   * </ion-list>
   * ```
   *
   * Advanced Usage: Thumbnails, Delete buttons, Reordering, Swiping
   *
   * ```html
   * <ion-list ng-controller="MyCtrl"
   *           show-delete="shouldShowDelete"
   *           show-reorder="shouldShowReorder"
   *           can-swipe="listCanSwipe">
   *   <ion-item ng-repeat="item in items"
   *             class="item-thumbnail-left">
   *
   *     {% raw %}<img ng-src="{{item.img}}">
   *     <h2>{{item.title}}</h2>
   *     <p>{{item.description}}</p>{% endraw %}
   *     <ion-option-button class="button-positive"
   *                        ng-click="share(item)">
   *       Share
   *     </ion-option-button>
   *     <ion-option-button class="button-info"
   *                        ng-click="edit(item)">
   *       Edit
   *     </ion-option-button>
   *     <ion-delete-button class="ion-minus-circled"
   *                        ng-click="items.splice($index, 1)">
   *     </ion-delete-button>
   *     <ion-reorder-button class="ion-navicon"
   *                         on-reorder="reorderItem(item, $fromIndex, $toIndex)">
   *     </ion-reorder-button>
   *
   *   </ion-item>
   * </ion-list>
   * ```
   *
   *```javascript
   * app.controller('MyCtrl', function($scope) {
   *  $scope.shouldShowDelete = false;
   *  $scope.shouldShowReorder = false;
   *  $scope.listCanSwipe = true
   * });
   *```
   *
   * @param {string=} delegate-handle The handle used to identify this list with
   * {@link ionic.service:$ionicListDelegate}.
   * @param type {string=} The type of list to use (list-inset or card)
   * @param show-delete {boolean=} Whether the delete buttons for the items in the list are
   * currently shown or hidden.
   * @param show-reorder {boolean=} Whether the reorder buttons for the items in the list are
   * currently shown or hidden.
   * @param can-swipe {boolean=} Whether the items in the list are allowed to be swiped to reveal
   * option buttons. Default: true.
   */
  IonicModule
  .directive('ionList', [
    '$timeout',
    function ($timeout) {
      return {
        restrict: 'E',
        require: ['ionList', '^?$ionicScroll'],
        controller: '$ionicList',
        compile: function ($element, $attr) {
          var listEl = jqLite('<div class="list">')
          .append($element.contents())
          .addClass($attr.type);

          $element.append(listEl);

          return function ($scope, $element, $attrs, ctrls) {
            var listCtrl = ctrls[0];
            var scrollCtrl = ctrls[1];

            // Wait for child elements to render...
            $timeout(init);

            function init() {
              var listView = listCtrl.listView = new ionic.views.ListView({
                el: $element[0],
                listEl: $element.children()[0],
                scrollEl: scrollCtrl && scrollCtrl.element,
                scrollView: scrollCtrl && scrollCtrl.scrollView,
                onReorder: function (el, oldIndex, newIndex) {
                  var itemScope = jqLite(el).scope();
                  if (itemScope && itemScope.$onReorder) {
                    // Make sure onReorder is called in apply cycle,
                    // but also make sure it has no conflicts by doing
                    // $evalAsync
                    $timeout(function () {
                      itemScope.$onReorder(oldIndex, newIndex);
                    });
                  }
                },
                canSwipe: function () {
                  return listCtrl.canSwipeItems();
                }
              });

              $scope.$on('$destroy', function () {
                if (listView) {
                  listView.deregister && listView.deregister();
                  listView = null;
                }
              });

              if (isDefined($attr.canSwipe)) {
                $scope.$watch('!!(' + $attr.canSwipe + ')', function (value) {
                  listCtrl.canSwipeItems(value);
                });
              }
              if (isDefined($attr.showDelete)) {
                $scope.$watch('!!(' + $attr.showDelete + ')', function (value) {
                  listCtrl.showDelete(value);
                });
              }
              if (isDefined($attr.showReorder)) {
                $scope.$watch('!!(' + $attr.showReorder + ')', function (value) {
                  listCtrl.showReorder(value);
                });
              }

              $scope.$watch(function () {
                return listCtrl.showDelete();
              }, function (isShown, wasShown) {
                //Only use isShown=false if it was already shown
                if (!isShown && !wasShown) {
                  return;
                }

                if (isShown) listCtrl.closeOptionButtons();
                listCtrl.canSwipeItems(!isShown);

                $element.children().toggleClass('list-left-editing', isShown);
                $element.toggleClass('disable-pointer-events', isShown);

                var deleteButton = jqLite($element[0].getElementsByClassName('item-delete'));
                setButtonShown(deleteButton, listCtrl.showDelete);
              });

              $scope.$watch(function () {
                return listCtrl.showReorder();
              }, function (isShown, wasShown) {
                //Only use isShown=false if it was already shown
                if (!isShown && !wasShown) {
                  return;
                }

                if (isShown) listCtrl.closeOptionButtons();
                listCtrl.canSwipeItems(!isShown);

                $element.children().toggleClass('list-right-editing', isShown);
                $element.toggleClass('disable-pointer-events', isShown);

                var reorderButton = jqLite($element[0].getElementsByClassName('item-reorder'));
                setButtonShown(reorderButton, listCtrl.showReorder);
              });

              function setButtonShown(el, shown) {
                shown() && el.addClass('visible') || el.removeClass('active');
                ionic.requestAnimationFrame(function () {
                  shown() && el.addClass('active') || el.removeClass('visible');
                });
              }
            }

          };
        }
      };
    }]);

  /**
   * @ngdoc directive
   * @name menuClose
   * @module ionic
   * @restrict AC
   *
   * @description
   * `menu-close` is an attribute directive that closes a currently opened side menu.
   * Note that by default, navigation transitions will not animate between views when
   * the menu is open. Additionally, this directive will reset the entering view's
   * history stack, making the new page the root of the history stack. This is done
   * to replicate the user experience seen in most side menu implementations, which is
   * to not show the back button at the root of the stack and show only the
   * menu button. We recommend that you also use the `enable-menu-with-back-views="false"`
   * {@link ionic.directive:ionSideMenus} attribute when using the menuClose directive.
   *
   * @usage
   * Below is an example of a link within a side menu. Tapping this link would
   * automatically close the currently opened menu.
   *
   * ```html
   * <a menu-close href="#/home" class="item">Home</a>
   * ```
   *
   * Note that if your destination state uses a resolve and that resolve asynchronously
   * takes longer than a standard transition (300ms), you'll need to set the
   * `nextViewOptions` manually as your resolve completes.
   *
   * ```js
   * $ionicHistory.nextViewOptions({
   *  historyRoot: true,
   *  disableAnimate: true,
   *  expire: 300
   * });
   * ```
   */
  IonicModule
  .directive('menuClose', ['$ionicHistory', '$timeout', function ($ionicHistory, $timeout) {
    return {
      restrict: 'AC',
      link: function ($scope, $element) {
        $element.bind('click', function () {
          var sideMenuCtrl = $element.inheritedData('$ionSideMenusController');
          if (sideMenuCtrl) {
            $ionicHistory.nextViewOptions({
              historyRoot: true,
              disableAnimate: true,
              expire: 300
            });
            // if no transition in 300ms, reset nextViewOptions
            // the expire should take care of it, but will be cancelled in some
            // cases. This directive is an exception to the rules of history.js
            $timeout(function () {
              $ionicHistory.nextViewOptions({
                historyRoot: false,
                disableAnimate: false
              });
            }, 300);
            sideMenuCtrl.close();
          }
        });
      }
    };
  }]);

  /**
   * @ngdoc directive
   * @name menuToggle
   * @module ionic
   * @restrict AC
   *
   * @description
   * Toggle a side menu on the given side.
   *
   * @usage
   * Below is an example of a link within a nav bar. Tapping this button
   * would open the given side menu, and tapping it again would close it.
   *
   * ```html
   * <ion-nav-bar>
   *   <ion-nav-buttons side="left">
   *    <!-- Toggle left side menu -->
   *    <button menu-toggle="left" class="button button-icon icon ion-navicon"></button>
   *   </ion-nav-buttons>
   *   <ion-nav-buttons side="right">
   *    <!-- Toggle right side menu -->
   *    <button menu-toggle="right" class="button button-icon icon ion-navicon"></button>
   *   </ion-nav-buttons>
   * </ion-nav-bar>
   * ```
   *
   * ### Button Hidden On Child Views
   * By default, the menu toggle button will only appear on a root
   * level side-menu page. Navigating in to child views will hide the menu-
   * toggle button. They can be made visible on child pages by setting the
   * enable-menu-with-back-views attribute of the {@link ionic.directive:ionSideMenus}
   * directive to true.
   *
   * ```html
   * <ion-side-menus enable-menu-with-back-views="true">
   * ```
   */
  IonicModule
  .directive('menuToggle', function () {
    return {
      restrict: 'AC',
      link: function ($scope, $element, $attr) {
        $scope.$on('$ionicView.beforeEnter', function (ev, viewData) {
          if (viewData.enableBack) {
            var sideMenuCtrl = $element.inheritedData('$ionSideMenusController');
            if (!sideMenuCtrl.enableMenuWithBackViews()) {
              $element.addClass('hide');
            }
          } else {
            $element.removeClass('hide');
          }
        });

        $element.bind('click', function () {
          var sideMenuCtrl = $element.inheritedData('$ionSideMenusController');
          sideMenuCtrl && sideMenuCtrl.toggle($attr.menuToggle);
        });
      }
    };
  });

  /*
 * We don't document the ionModal directive, we instead document
 * the $ionicModal service
 */
  IonicModule
  .directive('ionModal', [function () {
    return {
      restrict: 'E',
      transclude: true,
      replace: true,
      controller: [function () {
      }],
      template: '<div class="modal-backdrop">' +
        '<div class="modal-backdrop-bg"></div>' +
        '<div class="modal-wrapper" ng-transclude></div>' +
        '</div>'
    };
  }]);

  IonicModule
  .directive('ionModalView', function () {
    return {
      restrict: 'E',
      compile: function (element) {
        element.addClass('modal');
      }
    };
  });

  /**
   * @ngdoc directive
   * @name ionNavBackButton
   * @module ionic
   * @restrict E
   * @parent ionNavBar
   * @description
   * Creates a back button inside an {@link ionic.directive:ionNavBar}.
   *
   * The back button will appear when the user is able to go back in the current navigation stack. By
   * default, the markup of the back button is automatically built using platform-appropriate defaults
   * (iOS back button icon on iOS and Android icon on Android).
   *
   * Additionally, the button is automatically set to `$ionicGoBack()` on click/tap. By default, the
   * app will navigate back one view when the back button is clicked.  More advanced behavior is also
   * possible, as outlined below.
   *
   * @usage
   *
   * Recommended markup for default settings:
   *
   * ```html
   * <ion-nav-bar>
   *   <ion-nav-back-button>
   *   </ion-nav-back-button>
   * </ion-nav-bar>
   * ```
   *
   * With custom inner markup, and automatically adds a default click action:
   *
   * ```html
   * <ion-nav-bar>
   *   <ion-nav-back-button class="button-clear">
   *     <i class="ion-arrow-left-c"></i> Back
   *   </ion-nav-back-button>
   * </ion-nav-bar>
   * ```
   *
   * With custom inner markup and custom click action, using {@link ionic.service:$ionicHistory}:
   *
   * ```html
   * <ion-nav-bar ng-controller="MyCtrl">
   *   <ion-nav-back-button class="button-clear"
   *     ng-click="myGoBack()">
   *     <i class="ion-arrow-left-c"></i> Back
   *   </ion-nav-back-button>
   * </ion-nav-bar>
   * ```
   * ```js
   * function MyCtrl($scope, $ionicHistory) {
   *   $scope.myGoBack = function() {
   *     $ionicHistory.goBack();
   *   };
   * }
   * ```
   */
  IonicModule
  .directive('ionNavBackButton', ['$ionicConfig', '$document', function ($ionicConfig, $document) {
    return {
      restrict: 'E',
      require: '^ionNavBar',
      compile: function (tElement, tAttrs) {

        // clone the back button, but as a <div>
        var buttonEle = $document[0].createElement('button');
        for (var n in tAttrs.$attr) {
          buttonEle.setAttribute(tAttrs.$attr[n], tAttrs[n]);
        }

        if (!tAttrs.ngClick) {
          buttonEle.setAttribute('ng-click', '$ionicGoBack()');
        }

        buttonEle.className = 'button back-button hide buttons ' + (tElement.attr('class') || '');
        buttonEle.innerHTML = tElement.html() || '';

        var childNode;
        var hasIcon = hasIconClass(tElement[0]);
        var hasInnerText;
        var hasButtonText;
        var hasPreviousTitle;

        for (var x = 0; x < tElement[0].childNodes.length; x++) {
          childNode = tElement[0].childNodes[x];
          if (childNode.nodeType === 1) {
            if (hasIconClass(childNode)) {
              hasIcon = true;
            } else if (childNode.classList.contains('default-title')) {
              hasButtonText = true;
            } else if (childNode.classList.contains('previous-title')) {
              hasPreviousTitle = true;
            }
          } else if (!hasInnerText && childNode.nodeType === 3) {
            hasInnerText = !!childNode.nodeValue.trim();
          }
        }

        function hasIconClass(ele) {
          return /ion-|icon/.test(ele.className);
        }

        var defaultIcon = $ionicConfig.backButton.icon();
        if (!hasIcon && defaultIcon && defaultIcon !== 'none') {
          buttonEle.innerHTML = '<i class="icon ' + defaultIcon + '"></i> ' + buttonEle.innerHTML;
          buttonEle.className += ' button-clear';
        }

        if (!hasInnerText) {
          var buttonTextEle = $document[0].createElement('span');
          buttonTextEle.className = 'back-text';

          if (!hasButtonText && $ionicConfig.backButton.text()) {
            buttonTextEle.innerHTML += '<span class="default-title">' + $ionicConfig.backButton.text() + '</span>';
          }
          if (!hasPreviousTitle && $ionicConfig.backButton.previousTitleText()) {
            buttonTextEle.innerHTML += '<span class="previous-title"></span>';
          }
          buttonEle.appendChild(buttonTextEle);

        }

        tElement.attr('class', 'hide');
        tElement.empty();

        return {
          pre: function ($scope, $element, $attr, navBarCtrl) {
            // only register the plain HTML, the navBarCtrl takes care of scope/compile/link
            navBarCtrl.navElement('backButton', buttonEle.outerHTML);
            buttonEle = null;
          }
        };
      }
    };
  }]);


  /**
   * @ngdoc directive
   * @name ionNavBar
   * @module ionic
   * @delegate ionic.service:$ionicNavBarDelegate
   * @restrict E
   *
   * @description
   * If we have an {@link ionic.directive:ionNavView} directive, we can also create an
   * `<ion-nav-bar>`, which will create a topbar that updates as the application state changes.
   *
   * We can add a back button by putting an {@link ionic.directive:ionNavBackButton} inside.
   *
   * We can add buttons depending on the currently visible view using
   * {@link ionic.directive:ionNavButtons}.
   *
   * Note that the ion-nav-bar element will only work correctly if your content has an
   * ionView around it.
   *
   * @usage
   *
   * ```html
   * <body ng-app="starter">
   *   <!-- The nav bar that will be updated as we navigate -->
   *   <ion-nav-bar class="bar-positive">
   *   </ion-nav-bar>
   *
   *   <!-- where the initial view template will be rendered -->
   *   <ion-nav-view>
   *     <ion-view>
   *       <ion-content>Hello!</ion-content>
   *     </ion-view>
   *   </ion-nav-view>
   * </body>
   * ```
   *
   * @param {string=} delegate-handle The handle used to identify this navBar
   * with {@link ionic.service:$ionicNavBarDelegate}.
   * @param align-title {string=} Where to align the title of the navbar.
   * Available: 'left', 'right', 'center'. Defaults to 'center'.
   * @param {boolean=} no-tap-scroll By default, the navbar will scroll the content
   * to the top when tapped.  Set no-tap-scroll to true to disable this behavior.
   *
   */
  IonicModule
  .directive('ionNavBar', function () {
    return {
      restrict: 'E',
      controller: '$ionicNavBar',
      scope: true,
      link: function ($scope, $element, $attr, ctrl) {
        ctrl.init();
      }
    };
  });


  /**
   * @ngdoc directive
   * @name ionNavButtons
   * @module ionic
   * @restrict E
   * @parent ionNavView
   *
   * @description
   * Use nav buttons to set the buttons on your {@link ionic.directive:ionNavBar}
   * from within an {@link ionic.directive:ionView}. This gives each
   * view template the ability to specify which buttons should show in the nav bar,
   * overriding any default buttons already placed in the nav bar.
   *
   * Any buttons you declare will be positioned on the navbar's corresponding side. Primary
   * buttons generally map to the left side of the header, and secondary buttons are
   * generally on the right side. However, their exact locations are platform-specific.
   * For example, in iOS, the primary buttons are on the far left of the header, and
   * secondary buttons are on the far right, with the header title centered between them.
   * For Android, however, both groups of buttons are on the far right of the header,
   * with the header title aligned left.
   *
   * We recommend always using `primary` and `secondary`, so the buttons correctly map
   * to the side familiar to users of each platform. However, in cases where buttons should
   * always be on an exact side, both `left` and `right` sides are still available. For
   * example, a toggle button for a left side menu should be on the left side; in this case,
   * we'd recommend using `side="left"`, so it's always on the left, no matter the platform.
   *
   * ***Note*** that `ion-nav-buttons` must be immediate descendants of the `ion-view` or
   * `ion-nav-bar` element (basically, don't wrap it in another div).
   *
   * @usage
   * ```html
   * <ion-nav-bar>
   * </ion-nav-bar>
   * <ion-nav-view>
   *   <ion-view>
   *     <ion-nav-buttons side="primary">
   *       <button class="button" ng-click="doSomething()">
   *         I'm a button on the primary of the navbar!
   *       </button>
   *     </ion-nav-buttons>
   *     <ion-content>
   *       Some super content here!
   *     </ion-content>
   *   </ion-view>
   * </ion-nav-view>
   * ```
   *
   * @param {string} side The side to place the buttons in the
   * {@link ionic.directive:ionNavBar}. Available sides: `primary`, `secondary`, `left`, and `right`.
   */
  IonicModule
  .directive('ionNavButtons', ['$document', function ($document) {
    return {
      require: '^ionNavBar',
      restrict: 'E',
      compile: function (tElement, tAttrs) {
        var side = 'left';

        if (/^primary|secondary|right$/i.test(tAttrs.side || '')) {
          side = tAttrs.side.toLowerCase();
        }

        var spanEle = $document[0].createElement('span');
        spanEle.className = side + '-buttons';
        spanEle.innerHTML = tElement.html();

        var navElementType = side + 'Buttons';

        tElement.attr('class', 'hide');
        tElement.empty();

        return {
          pre: function ($scope, $element, $attrs, navBarCtrl) {
            // only register the plain HTML, the navBarCtrl takes care of scope/compile/link

            var parentViewCtrl = $element.parent().data('$ionViewController');
            if (parentViewCtrl) {
              // if the parent is an ion-view, then these are ion-nav-buttons for JUST this ion-view
              parentViewCtrl.navElement(navElementType, spanEle.outerHTML);

            } else {
              // these are buttons for all views that do not have their own ion-nav-buttons
              navBarCtrl.navElement(navElementType, spanEle.outerHTML);
            }

            spanEle = null;
          }
        };
      }
    };
  }]);

  /**
   * @ngdoc directive
   * @name navDirection
   * @module ionic
   * @restrict A
   *
   * @description
   * The direction which the nav view transition should animate. Available options
   * are: `forward`, `back`, `enter`, `exit`, `swap`.
   *
   * @usage
   *
   * ```html
   * <a nav-direction="forward" href="#/home">Home</a>
   * ```
   */
  IonicModule
  .directive('navDirection', ['$ionicViewSwitcher', function ($ionicViewSwitcher) {
    return {
      restrict: 'A',
      priority: 1000,
      link: function ($scope, $element, $attr) {
        $element.bind('click', function () {
          $ionicViewSwitcher.nextDirection($attr.navDirection);
        });
      }
    };
  }]);

  /**
   * @ngdoc directive
   * @name ionNavTitle
   * @module ionic
   * @restrict E
   * @parent ionNavView
   *
   * @description
   *
   * The nav title directive replaces an {@link ionic.directive:ionNavBar} title text with
   * custom HTML from within an {@link ionic.directive:ionView} template. This gives each
   * view the ability to specify its own custom title element, such as an image or any HTML,
   * rather than being text-only. Alternatively, text-only titles can be updated using the
   * `view-title` {@link ionic.directive:ionView} attribute.
   *
   * Note that `ion-nav-title` must be an immediate descendant of the `ion-view` or
   * `ion-nav-bar` element (basically don't wrap it in another div).
   *
   * @usage
   * ```html
   * <ion-nav-bar>
   * </ion-nav-bar>
   * <ion-nav-view>
   *   <ion-view>
   *     <ion-nav-title>
   *       <img src="logo.svg">
   *     </ion-nav-title>
   *     <ion-content>
   *       Some super content here!
   *     </ion-content>
   *   </ion-view>
   * </ion-nav-view>
   * ```
   *
   */
  IonicModule
  .directive('ionNavTitle', ['$document', function ($document) {
    return {
      require: '^ionNavBar',
      restrict: 'E',
      compile: function (tElement, tAttrs) {
        var navElementType = 'title';
        var spanEle = $document[0].createElement('span');
        for (var n in tAttrs.$attr) {
          spanEle.setAttribute(tAttrs.$attr[n], tAttrs[n]);
        }
        spanEle.classList.add('nav-bar-title');
        spanEle.innerHTML = tElement.html();

        tElement.attr('class', 'hide');
        tElement.empty();

        return {
          pre: function ($scope, $element, $attrs, navBarCtrl) {
            // only register the plain HTML, the navBarCtrl takes care of scope/compile/link

            var parentViewCtrl = $element.parent().data('$ionViewController');
            if (parentViewCtrl) {
              // if the parent is an ion-view, then these are ion-nav-buttons for JUST this ion-view
              parentViewCtrl.navElement(navElementType, spanEle.outerHTML);

            } else {
              // these are buttons for all views that do not have their own ion-nav-buttons
              navBarCtrl.navElement(navElementType, spanEle.outerHTML);
            }

            spanEle = null;
          }
        };
      }
    };
  }]);

  /**
   * @ngdoc directive
   * @name navTransition
   * @module ionic
   * @restrict A
   *
   * @description
   * The transition type which the nav view transition should use when it animates.
   * Current, options are `ios`, `android`, and `none`. More options coming soon.
   *
   * @usage
   *
   * ```html
   * <a nav-transition="none" href="#/home">Home</a>
   * ```
   */
  IonicModule
  .directive('navTransition', ['$ionicViewSwitcher', function ($ionicViewSwitcher) {
    return {
      restrict: 'A',
      priority: 1000,
      link: function ($scope, $element, $attr) {
        $element.bind('click', function () {
          $ionicViewSwitcher.nextTransition($attr.navTransition);
        });
      }
    };
  }]);

  /**
   * @ngdoc directive
   * @name ionNavView
   * @module ionic
   * @restrict E
   * @codepen odqCz
   *
   * @description
   * As a user navigates throughout your app, Ionic is able to keep track of their
   * navigation history. By knowing their history, transitions between views
   * correctly enter and exit using the platform's transition style. An additional
   * benefit to Ionic's navigation system is its ability to manage multiple
   * histories. For example, each tab can have it's own navigation history stack.
   *
   * Ionic uses the AngularUI Router module so app interfaces can be organized
   * into various "states". Like Angular's core $route service, URLs can be used
   * to control the views. However, the AngularUI Router provides a more powerful
   * state manager in that states are bound to named, nested, and parallel views,
   * allowing more than one template to be rendered on the same page.
   * Additionally, each state is not required to be bound to a URL, and data can
   * be pushed to each state which allows much flexibility.
   *
   * The ionNavView directive is used to render templates in your application. Each template
   * is part of a state. States are usually mapped to a url, and are defined programatically
   * using angular-ui-router (see [their docs](https://github.com/angular-ui/ui-router/wiki),
   * and remember to replace ui-view with ion-nav-view in examples).
   *
   * @usage
   * In this example, we will create a navigation view that contains our different states for the app.
   *
   * To do this, in our markup we use ionNavView top level directive. To display a header bar we use
   * the {@link ionic.directive:ionNavBar} directive that updates as we navigate through the
   * navigation stack.
   *
   * Next, we need to setup our states that will be rendered.
   *
   * ```js
   * var app = angular.module('learn-ionic-main', ['ionic']);
   * app.config(function($stateProvider) {
   *   $stateProvider
   *   .state('index', {
   *     url: '/',
   *     templateUrl: 'home.html'
   *   })
   *   .state('music', {
   *     url: '/music',
   *     templateUrl: 'music.html'
   *   });
   * });
   * ```
   * Then on app start, $stateProvider will look at the url, see if it matches the index state,
   * and then try to load home.html into the `<ion-nav-view>`.
   *
   * Pages are loaded by the URLs given. One simple way to create templates in Angular is to put
   * them directly into your HTML file and use the `<script type="text/ng-template">` syntax.
   * So here is one way to put home.html into our app:
   *
   * ```html
   * <script id="home" type="text/ng-template">
   *   <!-- The title of the ion-view will be shown on the navbar -->
   *   <ion-view view-title="Home">
   *     <ion-content ng-controller="HomeCtrl">
   *       <!-- The content of the page -->
   *       <a href="#/music">Go to music page!</a>
   *     </ion-content>
   *   </ion-view>
   * </script>
   * ```
   *
   * This is good to do because the template will be cached for very fast loading, instead of
   * having to fetch them from the network.
   *
   * ## Caching
   *
   * By default, views are cached to improve performance. When a view is navigated away from, its
   * element is left in the DOM, and its scope is disconnected from the `$watch` cycle. When
   * navigating to a view that is already cached, its scope is then reconnected, and the existing
   * element that was left in the DOM becomes the active view. This also allows for the scroll
   * position of previous views to be maintained.
   *
   * Caching can be disabled and enabled in multiple ways. By default, Ionic will cache a maximum of
   * 10 views, and not only can this be configured, but apps can also explicitly state which views
   * should and should not be cached.
   *
   * Note that because we are caching these views, *we aren’t destroying scopes*. Instead, scopes
   * are being disconnected from the watch cycle. Because scopes are not being destroyed and
   * recreated, controllers are not loading again on a subsequent viewing. If the app/controller
   * needs to know when a view has entered or has left, then view events emitted from the
   * {@link ionic.directive:ionView} scope, such as `$ionicView.enter`, may be useful.
   *
   * By default, when navigating back in the history, the "forward" views are removed from the cache.
   * If you navigate forward to the same view again, it'll create a new DOM element and controller
   * instance. Basically, any forward views are reset each time. This can be configured using the
   * {@link ionic.provider:$ionicConfigProvider}:
   *
   * ```js
   * $ionicConfigProvider.views.forwardCache(true);
   * ```
   *
   * #### Disable cache globally
   *
   * The {@link ionic.provider:$ionicConfigProvider} can be used to set the maximum allowable views
   * which can be cached, but this can also be use to disable all caching by setting it to 0.
   *
   * ```js
   * $ionicConfigProvider.views.maxCache(0);
   * ```
   *
   * #### Disable cache within state provider
   *
   * ```js
   * $stateProvider.state('myState', {
   *    cache: false,
   *    url : '/myUrl',
   *    templateUrl : 'my-template.html'
   * })
   * ```
   *
   * #### Disable cache with an attribute
   *
   * ```html
   * <ion-view cache-view="false" view-title="My Title!">
   *   ...
   * </ion-view>
   * ```
   *
   *
   * ## AngularUI Router
   *
   * Please visit [AngularUI Router's docs](https://github.com/angular-ui/ui-router/wiki) for
   * more info. Below is a great video by the AngularUI Router team that may help to explain
   * how it all works:
   *
   * <iframe width="560" height="315" src="//www.youtube.com/embed/dqJRoh8MnBo"
   * frameborder="0" allowfullscreen></iframe>
   *
   * Note: We do not recommend using [resolve](https://github.com/angular-ui/ui-router/wiki#resolve)
   * of AngularUI Router. The recommended approach is to execute any logic needed before beginning the state transition.
   *
   * @param {string=} name A view name. The name should be unique amongst the other views in the
   * same state. You can have views of the same name that live in different states. For more
   * information, see ui-router's
   * [ui-view documentation](http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.directive:ui-view).
   */
  IonicModule
  .directive('ionNavView', [
    '$state',
    '$ionicConfig',
    function ($state, $ionicConfig) {
      // IONIC's fork of Angular UI Router, v0.2.10
      // the navView handles registering views in the history and how to transition between them
      return {
        restrict: 'E',
        terminal: true,
        priority: 2000,
        transclude: true,
        controller: '$ionicNavView',
        compile: function (tElement, tAttrs, transclude) {

          // a nav view element is a container for numerous views
          tElement.addClass('view-container');
          ionic.DomUtil.cachedAttr(tElement, 'nav-view-transition', $ionicConfig.views.transition());

          return function ($scope, $element, $attr, navViewCtrl) {
            var latestLocals;

            // Put in the compiled initial view
            transclude($scope, function (clone) {
              $element.append(clone);
            });

            var viewData = navViewCtrl.init();

            // listen for $stateChangeSuccess
            $scope.$on('$stateChangeSuccess', function () {
              updateView(false);
            });
            $scope.$on('$viewContentLoading', function () {
              updateView(false);
            });

            // initial load, ready go
            updateView(true);


            function updateView(firstTime) {
              // get the current local according to the $state
              var viewLocals = $state.$current && $state.$current.locals[viewData.name];

              // do not update THIS nav-view if its is not the container for the given state
              // if the viewLocals are the same as THIS latestLocals, then nothing to do
              if (!viewLocals || (!firstTime && viewLocals === latestLocals)) return;

              // update the latestLocals
              latestLocals = viewLocals;
              viewData.state = viewLocals.$$state;

              // register, update and transition to the new view
              navViewCtrl.register(viewLocals);
            }

          };
        }
      };
    }]);

  IonicModule

  .config(['$provide', function ($provide) {
    $provide.decorator('ngClickDirective', ['$delegate', function ($delegate) {
      // drop the default ngClick directive
      $delegate.shift();
      return $delegate;
    }]);
  }])

  /**
   * @private
   */
  .factory('$ionicNgClick', ['$parse', function ($parse) {
    return function (scope, element, clickExpr) {
      var clickHandler = angular.isFunction(clickExpr) ?
        clickExpr :
        $parse(clickExpr);

      element.on('click', function (event) {
        scope.$apply(function () {
          clickHandler(scope, {$event: (event)});
        });
      });

      // Hack for iOS Safari's benefit. It goes searching for onclick handlers and is liable to click
      // something else nearby.
      element.onclick = noop;
    };
  }])

  .directive('ngClick', ['$ionicNgClick', function ($ionicNgClick) {
    return function (scope, element, attr) {
      $ionicNgClick(scope, element, attr.ngClick);
    };
  }])

  .directive('ionStopEvent', function () {
    return {
      restrict: 'A',
      link: function (scope, element, attr) {
        element.bind(attr.ionStopEvent, eventStopPropagation);
      }
    };
  });

  function eventStopPropagation(e) {
    e.stopPropagation();
  }


  /**
   * @ngdoc directive
   * @name ionPane
   * @module ionic
   * @restrict E
   *
   * @description A simple container that fits content, with no side effects.  Adds the 'pane' class to the element.
   */
  IonicModule
  .directive('ionPane', function () {
    return {
      restrict: 'E',
      link: function (scope, element) {
        element.addClass('pane');
      }
    };
  });

  /*
 * We don't document the ionPopover directive, we instead document
 * the $ionicPopover service
 */
  IonicModule
  .directive('ionPopover', [function () {
    return {
      restrict: 'E',
      transclude: true,
      replace: true,
      controller: [function () {
      }],
      template: '<div class="popover-backdrop">' +
        '<div class="popover-wrapper" ng-transclude></div>' +
        '</div>'
    };
  }]);

  IonicModule
  .directive('ionPopoverView', function () {
    return {
      restrict: 'E',
      compile: function (element) {
        element.append(jqLite('<div class="popover-arrow">'));
        element.addClass('popover');
      }
    };
  });

  /**
   * @ngdoc directive
   * @name ionRadio
   * @module ionic
   * @restrict E
   * @codepen saoBG
   * @description
   * The radio directive is no different than the HTML radio input, except it's styled differently.
   *
   * Radio behaves like [AngularJS radio](http://docs.angularjs.org/api/ng/input/input[radio]).
   *
   * @usage
   * ```html
   * <ion-radio ng-model="choice" ng-value="'A'">Choose A</ion-radio>
   * <ion-radio ng-model="choice" ng-value="'B'">Choose B</ion-radio>
   * <ion-radio ng-model="choice" ng-value="'C'">Choose C</ion-radio>
   * ```
   *
   * @param {string=} name The name of the radio input.
   * @param {expression=} value The value of the radio input.
   * @param {boolean=} disabled The state of the radio input.
   * @param {string=} icon The icon to use when the radio input is selected.
   * @param {expression=} ng-value Angular equivalent of the value attribute.
   * @param {expression=} ng-model The angular model for the radio input.
   * @param {boolean=} ng-disabled Angular equivalent of the disabled attribute.
   * @param {expression=} ng-change Triggers given expression when radio input's model changes
   */
  IonicModule
  .directive('ionRadio', function () {
    return {
      restrict: 'E',
      replace: true,
      require: '?ngModel',
      transclude: true,
      template:
        '<label class="item item-radio">' +
        '<input type="radio" name="radio-group">' +
        '<div class="radio-content">' +
        '<div class="item-content disable-pointer-events" ng-transclude></div>' +
        '<i class="radio-icon disable-pointer-events icon ion-checkmark"></i>' +
        '</div>' +
        '</label>',

      compile: function (element, attr) {
        if (attr.icon) {
          var iconElm = element.find('i');
          iconElm.removeClass('ion-checkmark').addClass(attr.icon);
        }

        var input = element.find('input');
        forEach({
          'name': attr.name,
          'value': attr.value,
          'disabled': attr.disabled,
          'ng-value': attr.ngValue,
          'ng-model': attr.ngModel,
          'ng-disabled': attr.ngDisabled,
          'ng-change': attr.ngChange,
          'ng-required': attr.ngRequired,
          'required': attr.required
        }, function (value, name) {
          if (isDefined(value)) {
            input.attr(name, value);
          }
        });

        return function (scope, element, attr) {
          scope.getValue = function () {
            return scope.ngValue || attr.value;
          };
        };
      }
    };
  });


  /**
   * @ngdoc directive
   * @name ionRefresher
   * @module ionic
   * @restrict E
   * @parent ionic.directive:ionContent, ionic.directive:ionScroll
   * @description
   * Allows you to add pull-to-refresh to a scrollView.
   *
   * Place it as the first child of your {@link ionic.directive:ionContent} or
   * {@link ionic.directive:ionScroll} element.
   *
   * When refreshing is complete, $broadcast the 'scroll.refreshComplete' event
   * from your controller.
   *
   * @usage
   *
   * ```html
   * <ion-content ng-controller="MyController">
   *   <ion-refresher
   *     pulling-text="Pull to refresh..."
   *     on-refresh="doRefresh()">
   *   </ion-refresher>
   *   <ion-list>
   *     <ion-item ng-repeat="item in items"></ion-item>
   *   </ion-list>
   * </ion-content>
   * ```
   * ```js
   * angular.module('testApp', ['ionic'])
   * .controller('MyController', function($scope, $http) {
   *   $scope.items = [1,2,3];
   *   $scope.doRefresh = function() {
   *     $http.get('/new-items')
   *      .success(function(newItems) {
   *        $scope.items = newItems;
   *      })
   *      .finally(function() {
   *        // Stop the ion-refresher from spinning
   *        $scope.$broadcast('scroll.refreshComplete');
   *      });
   *   };
   * });
   * ```
   *
   * @param {expression=} on-refresh Called when the user pulls down enough and lets go
   * of the refresher.
   * @param {expression=} on-pulling Called when the user starts to pull down
   * on the refresher.
   * @param {string=} pulling-text The text to display while the user is pulling down.
   * @param {string=} pulling-icon The icon to display while the user is pulling down.
   * Default: 'ion-android-arrow-down'.
   * @param {string=} spinner The {@link ionic.directive:ionSpinner} icon to display
   * after user lets go of the refresher. The SVG {@link ionic.directive:ionSpinner}
   * is now the default, replacing rotating font icons. Set to `none` to disable both the
   * spinner and the icon.
   * @param {string=} refreshing-icon The font icon to display after user lets go of the
   * refresher. This is deprecated in favor of the SVG {@link ionic.directive:ionSpinner}.
   * @param {boolean=} disable-pulling-rotation Disables the rotation animation of the pulling
   * icon when it reaches its activated threshold. To be used with a custom `pulling-icon`.
   *
   */
  IonicModule
  .directive('ionRefresher', [function () {
    return {
      restrict: 'E',
      replace: true,
      require: ['?^$ionicScroll', 'ionRefresher'],
      controller: '$ionicRefresher',
      template:
        '<div class="scroll-refresher invisible" collection-repeat-ignore>' +
        '<div class="ionic-refresher-content" ' +
        'ng-class="{\'ionic-refresher-with-text\': pullingText || refreshingText}">' +
        '<div class="icon-pulling" ng-class="{\'pulling-rotation-disabled\':disablePullingRotation}">' +
        '<i class="icon {{pullingIcon}}"></i>' +
        '</div>' +
        '<div class="text-pulling" ng-bind-html="pullingText"></div>' +
        '<div class="icon-refreshing">' +
        '<ion-spinner ng-if="showSpinner" icon="{{spinner}}"></ion-spinner>' +
        '<i ng-if="showIcon" class="icon {{refreshingIcon}}"></i>' +
        '</div>' +
        '<div class="text-refreshing" ng-bind-html="refreshingText"></div>' +
        '</div>' +
        '</div>',
      link: function ($scope, $element, $attrs, ctrls) {

        // JS Scrolling uses the scroll controller
        var scrollCtrl = ctrls[0],
          refresherCtrl = ctrls[1];
        if (!scrollCtrl || scrollCtrl.isNative()) {
          // Kick off native scrolling
          refresherCtrl.init();
        } else {
          $element[0].classList.add('js-scrolling');
          scrollCtrl._setRefresher(
            $scope,
            $element[0],
            refresherCtrl.getRefresherDomMethods()
          );

          $scope.$on('scroll.refreshComplete', function () {
            $scope.$evalAsync(function () {
              if (scrollCtrl.scrollView) {
                scrollCtrl.scrollView.finishPullToRefresh();
              }
            });
          });
        }

      }
    };
  }]);

  /**
   * @ngdoc directive
   * @name ionScroll
   * @module ionic
   * @delegate ionic.service:$ionicScrollDelegate
   * @codepen mwFuh
   * @restrict E
   *
   * @description
   * Creates a scrollable container for all content inside.
   *
   * @usage
   *
   * Basic usage:
   *
   * ```html
   * <ion-scroll zooming="true" direction="xy" style="width: 500px; height: 500px">
   *   <div style="width: 5000px; height: 5000px; background: url('https://upload.wikimedia.org/wikipedia/commons/a/ad/Europe_geological_map-en.jpg') repeat"></div>
   *  </ion-scroll>
   * ```
   *
   * Note that it's important to set the height of the scroll box as well as the height of the inner
   * content to enable scrolling. This makes it possible to have full control over scrollable areas.
   *
   * If you'd just like to have a center content scrolling area, use {@link ionic.directive:ionContent} instead.
   *
   * @param {string=} delegate-handle The handle used to identify this scrollView
   * with {@link ionic.service:$ionicScrollDelegate}.
   * @param {string=} direction Which way to scroll. 'x' or 'y' or 'xy'. Default 'y'.
   * @param {boolean=} locking Whether to lock scrolling in one direction at a time. Useful to set to false when zoomed in or scrolling in two directions. Default true.
   * @param {boolean=} paging Whether to scroll with paging.
   * @param {expression=} on-refresh Called on pull-to-refresh, triggered by an {@link ionic.directive:ionRefresher}.
   * @param {expression=} on-scroll Called whenever the user scrolls.
   * @param {expression=} on-scroll-complete Called whenever the scrolling paging is completed.
   * @param {boolean=} scrollbar-x Whether to show the horizontal scrollbar. Default true.
   * @param {boolean=} scrollbar-y Whether to show the vertical scrollbar. Default true.
   * @param {boolean=} zooming Whether to support pinch-to-zoom
   * @param {integer=} min-zoom The smallest zoom amount allowed (default is 0.5)
   * @param {integer=} max-zoom The largest zoom amount allowed (default is 3)
   * @param {boolean=} has-bouncing Whether to allow scrolling to bounce past the edges
   * of the content.  Defaults to true on iOS, false on Android.
   */
  IonicModule
  .directive('ionScroll', [
    '$timeout',
    '$controller',
    '$ionicBind',
    '$ionicConfig',
    function ($timeout, $controller, $ionicBind, $ionicConfig) {
      return {
        restrict: 'E',
        scope: true,
        controller: function () {
        },
        compile: function (element, attr) {
          element.addClass('scroll-view ionic-scroll');

          //We cannot transclude here because it breaks element.data() inheritance on compile
          var innerElement = jqLite('<div class="scroll"></div>');
          innerElement.append(element.contents());
          element.append(innerElement);

          var nativeScrolling = attr.overflowScroll !== "false" && (attr.overflowScroll === "true" || !$ionicConfig.scrolling.jsScrolling());

          return {pre: prelink};

          function prelink($scope, $element, $attr) {
            $ionicBind($scope, $attr, {
              direction: '@',
              paging: '@',
              $onScroll: '&onScroll',
              $onScrollComplete: '&onScrollComplete',
              scroll: '@',
              scrollbarX: '@',
              scrollbarY: '@',
              zooming: '@',
              minZoom: '@',
              maxZoom: '@'
            });
            $scope.direction = $scope.direction || 'y';

            if (isDefined($attr.padding)) {
              $scope.$watch($attr.padding, function (newVal) {
                innerElement.toggleClass('padding', !!newVal);
              });
            }
            if ($scope.$eval($scope.paging) === true) {
              innerElement.addClass('scroll-paging');
            }

            if (!$scope.direction) {
              $scope.direction = 'y';
            }
            var isPaging = $scope.$eval($scope.paging) === true;

            if (nativeScrolling) {
              $element.addClass('overflow-scroll');
            }

            $element.addClass('scroll-' + $scope.direction);

            var scrollViewOptions = {
              el: $element[0],
              delegateHandle: $attr.delegateHandle,
              locking: ($attr.locking || 'true') === 'true',
              bouncing: $scope.$eval($attr.hasBouncing),
              paging: isPaging,
              scrollbarX: $scope.$eval($scope.scrollbarX) !== false,
              scrollbarY: $scope.$eval($scope.scrollbarY) !== false,
              scrollingX: $scope.direction.indexOf('x') >= 0,
              scrollingY: $scope.direction.indexOf('y') >= 0,
              zooming: $scope.$eval($scope.zooming) === true,
              maxZoom: $scope.$eval($scope.maxZoom) || 3,
              minZoom: $scope.$eval($scope.minZoom) || 0.5,
              preventDefault: true,
              nativeScrolling: nativeScrolling,
              scrollingComplete: onScrollComplete
            };

            if (isPaging) {
              scrollViewOptions.speedMultiplier = 0.8;
              scrollViewOptions.bouncing = false;
            }

            var scrollCtrl = $controller('$ionicScroll', {
              $scope: $scope,
              scrollViewOptions: scrollViewOptions
            });

            function onScrollComplete() {
              $scope.$onScrollComplete && $scope.$onScrollComplete({
                scrollTop: scrollCtrl.scrollView.__scrollTop,
                scrollLeft: scrollCtrl.scrollView.__scrollLeft
              });
            }
          }
        }
      };
    }]);

  /**
   * @ngdoc directive
   * @name ionSideMenu
   * @module ionic
   * @restrict E
   * @parent ionic.directive:ionSideMenus
   *
   * @description
   * A container for a side menu, sibling to an {@link ionic.directive:ionSideMenuContent} directive.
   *
   * @usage
   * ```html
   * <ion-side-menu
   *   side="left"
   *   width="myWidthValue + 20"
   *   is-enabled="shouldLeftSideMenuBeEnabled()">
   * </ion-side-menu>
   * ```
   * For a complete side menu example, see the
   * {@link ionic.directive:ionSideMenus} documentation.
   *
   * @param {string} side Which side the side menu is currently on.  Allowed values: 'left' or 'right'.
   * @param {boolean=} is-enabled Whether this side menu is enabled.
   * @param {number=} width How many pixels wide the side menu should be.  Defaults to 275.
   */
  IonicModule
  .directive('ionSideMenu', function () {
    return {
      restrict: 'E',
      require: '^ionSideMenus',
      scope: true,
      compile: function (element, attr) {
        angular.isUndefined(attr.isEnabled) && attr.$set('isEnabled', 'true');
        angular.isUndefined(attr.width) && attr.$set('width', '275');

        element.addClass('menu menu-' + attr.side);

        return function ($scope, $element, $attr, sideMenuCtrl) {
          $scope.side = $attr.side || 'left';

          var sideMenu = sideMenuCtrl[$scope.side] = new ionic.views.SideMenu({
            width: attr.width,
            el: $element[0],
            isEnabled: true
          });

          $scope.$watch($attr.width, function (val) {
            var numberVal = +val;
            if (numberVal && numberVal == val) {
              sideMenu.setWidth(+val);
            }
          });
          $scope.$watch($attr.isEnabled, function (val) {
            sideMenu.setIsEnabled(!!val);
          });
        };
      }
    };
  });


  /**
   * @ngdoc directive
   * @name ionSideMenuContent
   * @module ionic
   * @restrict E
   * @parent ionic.directive:ionSideMenus
   *
   * @description
   * A container for the main visible content, sibling to one or more
   * {@link ionic.directive:ionSideMenu} directives.
   *
   * @usage
   * ```html
   * <ion-side-menu-content
   *   edge-drag-threshold="true"
   *   drag-content="true">
   * </ion-side-menu-content>
   * ```
   * For a complete side menu example, see the
   * {@link ionic.directive:ionSideMenus} documentation.
   *
   * @param {boolean=} drag-content Whether the content can be dragged. Default true.
   * @param {boolean|number=} edge-drag-threshold Whether the content drag can only start if it is below a certain threshold distance from the edge of the screen.  Default false. Accepts three types of values:
   *  - If a non-zero number is given, that many pixels is used as the maximum allowed distance from the edge that starts dragging the side menu.
   *  - If true is given, the default number of pixels (25) is used as the maximum allowed distance.
   *  - If false or 0 is given, the edge drag threshold is disabled, and dragging from anywhere on the content is allowed.
   *
   */
  IonicModule
  .directive('ionSideMenuContent', [
    '$timeout',
    '$ionicGesture',
    '$window',
    function ($timeout, $ionicGesture, $window) {

      return {
        restrict: 'EA', //DEPRECATED 'A'
        require: '^ionSideMenus',
        scope: true,
        compile: function (element, attr) {
          element.addClass('menu-content pane');

          return {pre: prelink};

          function prelink($scope, $element, $attr, sideMenuCtrl) {
            var startCoord = null;
            var primaryScrollAxis = null;

            if (isDefined(attr.dragContent)) {
              $scope.$watch(attr.dragContent, function (value) {
                sideMenuCtrl.canDragContent(value);
              });
            } else {
              sideMenuCtrl.canDragContent(true);
            }

            if (isDefined(attr.edgeDragThreshold)) {
              $scope.$watch(attr.edgeDragThreshold, function (value) {
                sideMenuCtrl.edgeDragThreshold(value);
              });
            }

            // Listen for taps on the content to close the menu
            function onContentTap(gestureEvt) {
              if (sideMenuCtrl.getOpenAmount() !== 0) {
                sideMenuCtrl.close();
                gestureEvt.gesture.srcEvent.preventDefault();
                startCoord = null;
                primaryScrollAxis = null;
              } else if (!startCoord) {
                startCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent);
              }
            }

            function onDragX(e) {
              if (!sideMenuCtrl.isDraggableTarget(e)) return;

              if (getPrimaryScrollAxis(e) == 'x') {
                sideMenuCtrl._handleDrag(e);
                e.gesture.srcEvent.preventDefault();
              }
            }

            function onDragY(e) {
              if (getPrimaryScrollAxis(e) == 'x') {
                e.gesture.srcEvent.preventDefault();
              }
            }

            function onDragRelease(e) {
              sideMenuCtrl._endDrag(e);
              startCoord = null;
              primaryScrollAxis = null;
            }

            function getPrimaryScrollAxis(gestureEvt) {
              // gets whether the user is primarily scrolling on the X or Y
              // If a majority of the drag has been on the Y since the start of
              // the drag, but the X has moved a little bit, it's still a Y drag

              if (primaryScrollAxis) {
                // we already figured out which way they're scrolling
                return primaryScrollAxis;
              }

              if (gestureEvt && gestureEvt.gesture) {

                if (!startCoord) {
                  // get the starting point
                  startCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent);

                } else {
                  // we already have a starting point, figure out which direction they're going
                  var endCoord = ionic.tap.pointerCoord(gestureEvt.gesture.srcEvent);

                  var xDistance = Math.abs(endCoord.x - startCoord.x);
                  var yDistance = Math.abs(endCoord.y - startCoord.y);

                  var scrollAxis = (xDistance < yDistance ? 'y' : 'x');

                  if (Math.max(xDistance, yDistance) > 30) {
                    // ok, we pretty much know which way they're going
                    // let's lock it in
                    primaryScrollAxis = scrollAxis;
                  }

                  return scrollAxis;
                }
              }
              return 'y';
            }

            var content = {
              element: element[0],
              onDrag: function () {
              },
              endDrag: function () {
              },
              setCanScroll: function (canScroll) {
                var c = $element[0].querySelector('.scroll');

                if (!c) {
                  return;
                }

                var content = angular.element(c.parentElement);
                if (!content) {
                  return;
                }

                // freeze our scroll container if we have one
                var scrollScope = content.scope();
                scrollScope.scrollCtrl && scrollScope.scrollCtrl.freezeScrollShut(!canScroll);
              },
              getTranslateX: function () {
                return $scope.sideMenuContentTranslateX || 0;
              },
              setTranslateX: ionic.animationFrameThrottle(function (amount) {
                var xTransform = content.offsetX + amount;
                $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + xTransform + 'px,0,0)';
                $timeout(function () {
                  $scope.sideMenuContentTranslateX = amount;
                });
              }),
              setMarginLeft: ionic.animationFrameThrottle(function (amount) {
                if (amount) {
                  amount = parseInt(amount, 10);
                  $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + amount + 'px,0,0)';
                  $element[0].style.width = ($window.innerWidth - amount) + 'px';
                  content.offsetX = amount;
                } else {
                  $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
                  $element[0].style.width = '';
                  content.offsetX = 0;
                }
              }),
              setMarginRight: ionic.animationFrameThrottle(function (amount) {
                if (amount) {
                  amount = parseInt(amount, 10);
                  $element[0].style.width = ($window.innerWidth - amount) + 'px';
                  content.offsetX = amount;
                } else {
                  $element[0].style.width = '';
                  content.offsetX = 0;
                }
                // reset incase left gets grabby
                $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
              }),
              setMarginLeftAndRight: ionic.animationFrameThrottle(function (amountLeft, amountRight) {
                amountLeft = amountLeft && parseInt(amountLeft, 10) || 0;
                amountRight = amountRight && parseInt(amountRight, 10) || 0;

                var amount = amountLeft + amountRight;

                if (amount > 0) {
                  $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(' + amountLeft + 'px,0,0)';
                  $element[0].style.width = ($window.innerWidth - amount) + 'px';
                  content.offsetX = amountLeft;
                } else {
                  $element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
                  $element[0].style.width = '';
                  content.offsetX = 0;
                }
                // reset incase left gets grabby
                //$element[0].style[ionic.CSS.TRANSFORM] = 'translate3d(0,0,0)';
              }),
              enableAnimation: function () {
                $scope.animationEnabled = true;
                $element[0].classList.add('menu-animated');
              },
              disableAnimation: function () {
                $scope.animationEnabled = false;
                $element[0].classList.remove('menu-animated');
              },
              offsetX: 0
            };

            sideMenuCtrl.setContent(content);

            // add gesture handlers
            var gestureOpts = {stop_browser_behavior: false};
            gestureOpts.prevent_default_directions = ['left', 'right'];
            var contentTapGesture = $ionicGesture.on('tap', onContentTap, $element, gestureOpts);
            var dragRightGesture = $ionicGesture.on('dragright', onDragX, $element, gestureOpts);
            var dragLeftGesture = $ionicGesture.on('dragleft', onDragX, $element, gestureOpts);
            var dragUpGesture = $ionicGesture.on('dragup', onDragY, $element, gestureOpts);
            var dragDownGesture = $ionicGesture.on('dragdown', onDragY, $element, gestureOpts);
            var releaseGesture = $ionicGesture.on('release', onDragRelease, $element, gestureOpts);

            // Cleanup
            $scope.$on('$destroy', function () {
              if (content) {
                content.element = null;
                content = null;
              }
              $ionicGesture.off(dragLeftGesture, 'dragleft', onDragX);
              $ionicGesture.off(dragRightGesture, 'dragright', onDragX);
              $ionicGesture.off(dragUpGesture, 'dragup', onDragY);
              $ionicGesture.off(dragDownGesture, 'dragdown', onDragY);
              $ionicGesture.off(releaseGesture, 'release', onDragRelease);
              $ionicGesture.off(contentTapGesture, 'tap', onContentTap);
            });
          }
        }
      };
    }]);

  IonicModule

  /**
   * @ngdoc directive
   * @name ionSideMenus
   * @module ionic
   * @delegate ionic.service:$ionicSideMenuDelegate
   * @restrict E
   *
   * @description
   * A container element for side menu(s) and the main content. Allows the left and/or right side menu
   * to be toggled by dragging the main content area side to side.
   *
   * To automatically close an opened menu, you can add the {@link ionic.directive:menuClose} attribute
   * directive. The `menu-close` attribute is usually added to links and buttons within
   * `ion-side-menu-content`, so that when the element is clicked, the opened side menu will
   * automatically close.
   *
   * "Burger Icon" toggles can be added to the header with the {@link ionic.directive:menuToggle}
   * attribute directive. Clicking the toggle will open and close the side menu like the `menu-close`
   * directive. The side menu will automatically hide on child pages, but can be overridden with the
   * enable-menu-with-back-views attribute mentioned below.
   *
   * By default, side menus are hidden underneath their side menu content and can be opened by swiping
   * the content left or right or by toggling a button to show the side menu. Additionally, by adding the
   * {@link ionic.directive:exposeAsideWhen} attribute directive to an
   * {@link ionic.directive:ionSideMenu} element directive, a side menu can be given instructions about
   * "when" the menu should be exposed (always viewable).
   *
   * ![Side Menu](http://ionicframework.com.s3.amazonaws.com/docs/controllers/sidemenu.gif)
   *
   * For more information on side menus, check out:
   *
   * - {@link ionic.directive:ionSideMenuContent}
   * - {@link ionic.directive:ionSideMenu}
   * - {@link ionic.directive:menuToggle}
   * - {@link ionic.directive:menuClose}
   * - {@link ionic.directive:exposeAsideWhen}
   *
   * @usage
   * To use side menus, add an `<ion-side-menus>` parent element. This will encompass all pages that have a
   * side menu, and have at least 2 child elements: 1 `<ion-side-menu-content>` for the center content,
   * and one or more `<ion-side-menu>` directives for each side menu(left/right) that you wish to place.
   *
   * ```html
   * <ion-side-menus>
   *   <!-- Left menu -->
   *   <ion-side-menu side="left">
   *   </ion-side-menu>
   *
   *   <ion-side-menu-content>
   *   <!-- Main content, usually <ion-nav-view> -->
   *   </ion-side-menu-content>
   *
   *   <!-- Right menu -->
   *   <ion-side-menu side="right">
   *   </ion-side-menu>
   *
   * </ion-side-menus>
   * ```
   * ```js
   * function ContentController($scope, $ionicSideMenuDelegate) {
   *   $scope.toggleLeft = function() {
   *     $ionicSideMenuDelegate.toggleLeft();
   *   };
   * }
   * ```
   *
   * @param {bool=} enable-menu-with-back-views Determines whether the side menu is enabled when the
   * back button is showing. When set to `false`, any {@link ionic.directive:menuToggle} will be hidden,
   * and the user cannot swipe to open the menu. When going back to the root page of the side menu (the
   * page without a back button visible), then any menuToggle buttons will show again, and menus will be
   * enabled again.
   * @param {string=} delegate-handle The handle used to identify this side menu
   * with {@link ionic.service:$ionicSideMenuDelegate}.
   *
   */
  .directive('ionSideMenus', ['$ionicBody', function ($ionicBody) {
    return {
      restrict: 'ECA',
      controller: '$ionicSideMenus',
      compile: function (element, attr) {
        attr.$set('class', (attr['class'] || '') + ' view');

        return {pre: prelink};

        function prelink($scope, $element, $attrs, ctrl) {

          ctrl.enableMenuWithBackViews($scope.$eval($attrs.enableMenuWithBackViews));

          $scope.$on('$ionicExposeAside', function (evt, isAsideExposed) {
            if (!$scope.$exposeAside) $scope.$exposeAside = {};
            $scope.$exposeAside.active = isAsideExposed;
            $ionicBody.enableClass(isAsideExposed, 'aside-open');
          });

          $scope.$on('$ionicView.beforeEnter', function (ev, d) {
            if (d.historyId) {
              $scope.$activeHistoryId = d.historyId;
            }
          });

          $scope.$on('$destroy', function () {
            $ionicBody.removeClass('menu-open', 'aside-open');
          });

        }
      }
    };
  }]);


  /**
   * @ngdoc directive
   * @name ionSlideBox
   * @module ionic
   * @codepen AjgEB
   * @deprecated will be removed in the next Ionic release in favor of the new ion-slides component.
   * Don't depend on the internal behavior of this widget.
   * @delegate ionic.service:$ionicSlideBoxDelegate
   * @restrict E
   * @description
   * The Slide Box is a multi-page container where each page can be swiped or dragged between:
   *
   *
   * @usage
   * ```html
   * <ion-slide-box on-slide-changed="slideHasChanged($index)">
   *   <ion-slide>
   *     <div class="box blue"><h1>BLUE</h1></div>
   *   </ion-slide>
   *   <ion-slide>
   *     <div class="box yellow"><h1>YELLOW</h1></div>
   *   </ion-slide>
   *   <ion-slide>
   *     <div class="box pink"><h1>PINK</h1></div>
   *   </ion-slide>
   * </ion-slide-box>
   * ```
   *
   * @param {string=} delegate-handle The handle used to identify this slideBox
   * with {@link ionic.service:$ionicSlideBoxDelegate}.
   * @param {boolean=} does-continue Whether the slide box should loop.
   * @param {boolean=} auto-play Whether the slide box should automatically slide. Default true if does-continue is true.
   * @param {number=} slide-interval How many milliseconds to wait to change slides (if does-continue is true). Defaults to 4000.
   * @param {boolean=} show-pager Whether a pager should be shown for this slide box. Accepts expressions via `show-pager="{{shouldShow()}}"`. Defaults to true.
   * @param {expression=} pager-click Expression to call when a pager is clicked (if show-pager is true). Is passed the 'index' variable.
   * @param {expression=} on-slide-changed Expression called whenever the slide is changed.  Is passed an '$index' variable.
   * @param {expression=} active-slide Model to bind the current slide index to.
   */
  IonicModule
  .directive('ionSlideBox', [
    '$animate',
    '$timeout',
    '$compile',
    '$ionicSlideBoxDelegate',
    '$ionicHistory',
    '$ionicScrollDelegate',
    function ($animate, $timeout, $compile, $ionicSlideBoxDelegate, $ionicHistory, $ionicScrollDelegate) {
      return {
        restrict: 'E',
        replace: true,
        transclude: true,
        scope: {
          autoPlay: '=',
          doesContinue: '@',
          slideInterval: '@',
          showPager: '@',
          pagerClick: '&',
          disableScroll: '@',
          onSlideChanged: '&',
          activeSlide: '=?',
          bounce: '@'
        },
        controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
          var _this = this;

          var continuous = $scope.$eval($scope.doesContinue) === true;
          var bouncing = ($scope.$eval($scope.bounce) !== false); //Default to true
          var shouldAutoPlay = isDefined($attrs.autoPlay) ? !!$scope.autoPlay : false;
          var slideInterval = shouldAutoPlay ? $scope.$eval($scope.slideInterval) || 4000 : 0;

          var slider = new ionic.views.Slider({
            el: $element[0],
            auto: slideInterval,
            continuous: continuous,
            startSlide: $scope.activeSlide,
            bouncing: bouncing,
            slidesChanged: function () {
              $scope.currentSlide = slider.currentIndex();

              // Try to trigger a digest
              $timeout(function () {
              });
            },
            callback: function (slideIndex) {
              $scope.currentSlide = slideIndex;
              $scope.onSlideChanged({index: $scope.currentSlide, $index: $scope.currentSlide});
              $scope.$parent.$broadcast('slideBox.slideChanged', slideIndex);
              $scope.activeSlide = slideIndex;
              // Try to trigger a digest
              $timeout(function () {
              });
            },
            onDrag: function () {
              freezeAllScrolls(true);
            },
            onDragEnd: function () {
              freezeAllScrolls(false);
            }
          });

          function freezeAllScrolls(shouldFreeze) {
            if (shouldFreeze && !_this.isScrollFreeze) {
              $ionicScrollDelegate.freezeAllScrolls(shouldFreeze);

            } else if (!shouldFreeze && _this.isScrollFreeze) {
              $ionicScrollDelegate.freezeAllScrolls(false);
            }
            _this.isScrollFreeze = shouldFreeze;
          }

          slider.enableSlide($scope.$eval($attrs.disableScroll) !== true);

          $scope.$watch('activeSlide', function (nv) {
            if (isDefined(nv)) {
              slider.slide(nv);
            }
          });

          $scope.$on('slideBox.nextSlide', function () {
            slider.next();
          });

          $scope.$on('slideBox.prevSlide', function () {
            slider.prev();
          });

          $scope.$on('slideBox.setSlide', function (e, index) {
            slider.slide(index);
          });

          //Exposed for testing
          this.__slider = slider;

          var deregisterInstance = $ionicSlideBoxDelegate._registerInstance(
            slider, $attrs.delegateHandle, function () {
              return $ionicHistory.isActiveScope($scope);
            }
          );
          $scope.$on('$destroy', function () {
            deregisterInstance();
            slider.kill();
          });

          this.slidesCount = function () {
            return slider.slidesCount();
          };

          this.onPagerClick = function (index) {
            $scope.pagerClick({index: index});
          };

          $timeout(function () {
            slider.load();
          });
        }],
        template: '<div class="slider">' +
          '<div class="slider-slides" ng-transclude>' +
          '</div>' +
          '</div>',

        link: function ($scope, $element, $attr) {
          // Disable ngAnimate for slidebox and its children
          $animate.enabled($element, false);

          // if showPager is undefined, show the pager
          if (!isDefined($attr.showPager)) {
            $scope.showPager = true;
            getPager().toggleClass('hide', !true);
          }

          $attr.$observe('showPager', function (show) {
            if (show === undefined) return;
            show = $scope.$eval(show);
            getPager().toggleClass('hide', !show);
          });

          var pager;

          function getPager() {
            if (!pager) {
              var childScope = $scope.$new();
              pager = jqLite('<ion-pager></ion-pager>');
              $element.append(pager);
              pager = $compile(pager)(childScope);
            }
            return pager;
          }
        }
      };
    }])
  .directive('ionSlide', function () {
    return {
      restrict: 'E',
      require: '?^ionSlideBox',
      compile: function (element) {
        element.addClass('slider-slide');
      }
    };
  })

  .directive('ionPager', function () {
    return {
      restrict: 'E',
      replace: true,
      require: '^ionSlideBox',
      template: '<div class="slider-pager"><span class="slider-pager-page" ng-repeat="slide in numSlides() track by $index" ng-class="{active: $index == currentSlide}" ng-click="pagerClick($index)"><i class="icon ion-record"></i></span></div>',
      link: function ($scope, $element, $attr, slideBox) {
        var selectPage = function (index) {
          var children = $element[0].children;
          var length = children.length;
          for (var i = 0; i < length; i++) {
            if (i == index) {
              children[i].classList.add('active');
            } else {
              children[i].classList.remove('active');
            }
          }
        };

        $scope.pagerClick = function (index) {
          slideBox.onPagerClick(index);
        };

        $scope.numSlides = function () {
          return new Array(slideBox.slidesCount());
        };

        $scope.$watch('currentSlide', function (v) {
          selectPage(v);
        });
      }
    };

  });


  /**
   * @ngdoc directive
   * @name ionSlides
   * @module ionic
   * @restrict E
   * @description
   * The Slides component is a powerful multi-page container where each page can be swiped or dragged between.
   *
   * Note: this is a new version of the Ionic Slide Box based on the [Swiper](http://www.idangero.us/swiper/#.Vmc1J-ODFBc) widget from
   * [idangerous](http://www.idangero.us/).
   *
   * ![SlideBox](http://ionicframework.com.s3.amazonaws.com/docs/controllers/slideBox.gif)
   *
   * @usage
   * ```html
   * <ion-content scroll="false">
   *   <ion-slides  options="options" slider="data.slider">
   *     <ion-slide-page>
   *       <div class="box blue"><h1>BLUE</h1></div>
   *     </ion-slide-page>
   *     <ion-slide-page>
   *       <div class="box yellow"><h1>YELLOW</h1></div>
   *     </ion-slide-page>
   *     <ion-slide-page>
   *       <div class="box pink"><h1>PINK</h1></div>
   *     </ion-slide-page>
   *   </ion-slides>
   * </ion-content>
   * ```
   *
   * ```js
   * $scope.options = {
   *   loop: false,
   *   effect: 'fade',
   *   speed: 500,
   * }
   *
   * $scope.$on("$ionicSlides.sliderInitialized", function(event, data){
   *   // data.slider is the instance of Swiper
   *   $scope.slider = data.slider;
   * });
   *
   * $scope.$on("$ionicSlides.slideChangeStart", function(event, data){
   *   console.log('Slide change is beginning');
   * });
   *
   * $scope.$on("$ionicSlides.slideChangeEnd", function(event, data){
   *   // note: the indexes are 0-based
   *   $scope.activeIndex = data.slider.activeIndex;
   *   $scope.previousIndex = data.slider.previousIndex;
   * });
   *
   * ```
   *
   * ## Slide Events
   *
   * The slides component dispatches events when the active slide changes
   *
   * <table class="table">
   *   <tr>
   *     <td><code>$ionicSlides.slideChangeStart</code></td>
   *     <td>This event is emitted when a slide change begins</td>
   *   </tr>
   *   <tr>
   *     <td><code>$ionicSlides.slideChangeEnd</code></td>
   *     <td>This event is emitted when a slide change completes</td>
   *   </tr>
   *   <tr>
   *     <td><code>$ionicSlides.sliderInitialized</code></td>
   *     <td>This event is emitted when the slider is initialized. It provides access to an instance of the slider.</td>
   *   </tr>
   * </table>
   *
   *
   * ## Updating Slides Dynamically
   * When applying data to the slider at runtime, typically everything will work as expected.
   *
   * In the event that the slides are looped, use the `updateLoop` method on the slider to ensure the slides update correctly.
   *
   * ```
   * $scope.$on("$ionicSlides.sliderInitialized", function(event, data){
   *   // grab an instance of the slider
   *   $scope.slider = data.slider;
   * });
   *
   * function dataChangeHandler(){
   *   // call this function when data changes, such as an HTTP request, etc
   *   if ( $scope.slider ){
   *     $scope.slider.updateLoop();
   *   }
   * }
   * ```
   *
   */
  IonicModule
  .directive('ionSlides', [
    '$animate',
    '$timeout',
    '$compile',
    function ($animate, $timeout, $compile) {
      return {
        restrict: 'E',
        transclude: true,
        scope: {
          options: '=',
          slider: '='
        },
        template: '<div class="swiper-container">' +
          '<div class="swiper-wrapper" ng-transclude>' +
          '</div>' +
          '<div ng-hide="!showPager" class="swiper-pagination"></div>' +
          '</div>',
        controller: ['$scope', '$element', function ($scope, $element) {
          var _this = this;

          this.update = function () {
            $timeout(function () {
              if (!_this.__slider) {
                return;
              }

              _this.__slider.update();
              if (_this._options.loop) {
                _this.__slider.createLoop();
              }

              var slidesLength = _this.__slider.slides.length;

              // Don't allow pager to show with > 10 slides
              if (slidesLength > 10) {
                $scope.showPager = false;
              }

              // When slide index is greater than total then slide to last index
              if (_this.__slider.activeIndex > slidesLength - 1) {
                _this.__slider.slideTo(slidesLength - 1);
              }
            });
          };

          this.rapidUpdate = ionic.debounce(function () {
            _this.update();
          }, 50);

          this.getSlider = function () {
            return _this.__slider;
          };

          var options = $scope.options || {};

          var newOptions = angular.extend({
            pagination: $element.children().children()[1],
            paginationClickable: true,
            lazyLoading: true,
            preloadImages: false
          }, options);

          this._options = newOptions;

          $timeout(function () {
            var slider = new ionic.views.Swiper($element.children()[0], newOptions, $scope, $compile);

            $scope.$emit("$ionicSlides.sliderInitialized", {slider: slider});

            _this.__slider = slider;
            $scope.slider = _this.__slider;

            $scope.$on('$destroy', function () {
              slider.destroy();
              _this.__slider = null;
            });
          });

          $timeout(function () {
            // if it's a loop, render the slides again just incase
            _this.rapidUpdate();
          }, 200);

        }],

        link: function ($scope) {
          $scope.showPager = true;
          // Disable ngAnimate for slidebox and its children
          //$animate.enabled(false, $element);
        }
      };
    }])
  .directive('ionSlidePage', [function () {
    return {
      restrict: 'E',
      require: '?^ionSlides',
      transclude: true,
      replace: true,
      template: '<div class="swiper-slide" ng-transclude></div>',
      link: function ($scope, $element, $attr, ionSlidesCtrl) {
        ionSlidesCtrl.rapidUpdate();

        $scope.$on('$destroy', function () {
          ionSlidesCtrl.rapidUpdate();
        });
      }
    };
  }]);

  /**
   * @ngdoc directive
   * @name ionSpinner
   * @module ionic
   * @restrict E
   *
   * @description
   * The `ionSpinner` directive provides a variety of animated spinners.
   * Spinners enables you to give your users feedback that the app is
   * processing/thinking/waiting/chillin' out, or whatever you'd like it to indicate.
   * By default, the {@link ionic.directive:ionRefresher} feature uses this spinner, rather
   * than rotating font icons (previously included in [ionicons](http://ionicons.com/)).
   * While font icons are great for simple or stationary graphics, they're not suited to
   * provide great animations, which is why Ionic uses SVG instead.
   *
   * Ionic offers ten spinners out of the box, and by default, it will use the appropriate spinner
   * for the platform on which it's running. Under the hood, the `ionSpinner` directive dynamically
   * builds the required SVG element, which allows Ionic to provide all ten of the animated SVGs
   * within 3KB.
   *
   * <style>
   * .spinner-table {
   *   max-width: 280px;
   * }
   * .spinner-table tbody > tr > th, .spinner-table tbody > tr > td {
   *   vertical-align: middle;
   *   width: 42px;
   *   height: 42px;
   * }
   * .spinner {
   *   stroke: #444;
   *   fill: #444; }
   *   .spinner svg {
   *     width: 28px;
   *     height: 28px; }
   *   .spinner.spinner-inverse {
   *     stroke: #fff;
   *     fill: #fff; }
   *
   * .spinner-android {
   *   stroke: #4b8bf4; }
   *
   * .spinner-ios, .spinner-ios-small {
   *   stroke: #69717d; }
   *
   * .spinner-spiral .stop1 {
   *   stop-color: #fff;
   *   stop-opacity: 0; }
   * .spinner-spiral.spinner-inverse .stop1 {
   *   stop-color: #000; }
   * .spinner-spiral.spinner-inverse .stop2 {
   *   stop-color: #fff; }
   * </style>
   *
   * <script src="http://code.ionicframework.com/nightly/js/ionic.bundle.min.js"></script>
   * <table class="table spinner-table" ng-app="ionic">
   *  <tr>
   *    <th>
   *      <code>android</code>
   *    </th>
   *    <td>
   *      <ion-spinner icon="android"></ion-spinner>
   *    </td>
   *  </tr>
   *  <tr>
   *    <th>
   *      <code>ios</code>
   *    </th>
   *    <td>
   *      <ion-spinner icon="ios"></ion-spinner>
   *    </td>
   *  </tr>
   *  <tr>
   *    <th>
   *      <code>ios-small</code>
   *    </th>
   *    <td>
   *      <ion-spinner icon="ios-small"></ion-spinner>
   *    </td>
   *  </tr>
   *  <tr>
   *    <th>
   *      <code>bubbles</code>
   *    </th>
   *    <td>
   *      <ion-spinner icon="bubbles"></ion-spinner>
   *    </td>
   *  </tr>
   *  <tr>
   *    <th>
   *      <code>circles</code>
   *    </th>
   *    <td>
   *      <ion-spinner icon="circles"></ion-spinner>
   *    </td>
   *  </tr>
   *  <tr>
   *    <th>
   *      <code>crescent</code>
   *    </th>
   *    <td>
   *      <ion-spinner icon="crescent"></ion-spinner>
   *    </td>
   *  </tr>
   *  <tr>
   *    <th>
   *      <code>dots</code>
   *    </th>
   *    <td>
   *      <ion-spinner icon="dots"></ion-spinner>
   *    </td>
   *  </tr>
   *  <tr>
   *    <th>
   *      <code>lines</code>
   *    </th>
   *    <td>
   *      <ion-spinner icon="lines"></ion-spinner>
   *    </td>
   *  </tr>
   *  <tr>
   *    <th>
   *      <code>ripple</code>
   *    </th>
   *    <td>
   *      <ion-spinner icon="ripple"></ion-spinner>
   *    </td>
   *  </tr>
   *  <tr>
   *    <th>
   *      <code>spiral</code>
   *    </th>
   *    <td>
   *      <ion-spinner icon="spiral"></ion-spinner>
   *    </td>
   *  </tr>
   * </table>
   *
   * Each spinner uses SVG with SMIL animations, however, the Android spinner also uses JavaScript
   * so it also works on Android 4.0-4.3. Additionally, each spinner can be styled with CSS,
   * and scaled to any size.
   *
   *
   * @usage
   * The following code would use the default spinner for the platform it's running from. If it's neither
   * iOS or Android, it'll default to use `ios`.
   *
   * ```html
   * <ion-spinner></ion-spinner>
   * ```
   *
   * By setting the `icon` attribute, you can specify which spinner to use, no matter what
   * the platform is.
   *
   * ```html
   * <ion-spinner icon="spiral"></ion-spinner>
   * ```
   *
   * ## Spinner Colors
   * Like with most of Ionic's other components, spinners can also be styled using
   * Ionic's standard color naming convention. For example:
   *
   * ```html
   * <ion-spinner class="spinner-energized"></ion-spinner>
   * ```
   *
   *
   * ## Styling SVG with CSS
   * One cool thing about SVG is its ability to be styled with CSS! Some of the properties
   * have different names, for example, SVG uses the term `stroke` instead of `border`, and
   * `fill` instead of `background-color`.
   *
   * ```css
   * .spinner svg {
   *   width: 28px;
   *   height: 28px;
   *   stroke: #444;
   *   fill: #444;
   * }
   * ```
   *
   */
  IonicModule
  .directive('ionSpinner', function () {
    return {
      restrict: 'E',
      controller: '$ionicSpinner',
      link: function ($scope, $element, $attrs, ctrl) {
        var spinnerName = ctrl.init();
        $element.addClass('spinner spinner-' + spinnerName);

        $element.on('$destroy', function onDestroy() {
          ctrl.stop();
        });
      }
    };
  });

  /**
   * @ngdoc directive
   * @name ionTab
   * @module ionic
   * @restrict E
   * @parent ionic.directive:ionTabs
   *
   * @description
   * Contains a tab's content.  The content only exists while the given tab is selected.
   *
   * Each ionTab has its own view history.
   *
   * @usage
   * ```html
   * <ion-tab
   *   title="Tab!"
   *   icon="my-icon"
   *   href="#/tab/tab-link"
   *   on-select="onTabSelected()"
   *   on-deselect="onTabDeselected()">
   * </ion-tab>
   * ```
   * For a complete, working tab bar example, see the {@link ionic.directive:ionTabs} documentation.
   *
   * @param {string} title The title of the tab.
   * @param {string=} href The link that this tab will navigate to when tapped.
   * @param {string=} icon The icon of the tab. If given, this will become the default for icon-on and icon-off.
   * @param {string=} icon-on The icon of the tab while it is selected.
   * @param {string=} icon-off The icon of the tab while it is not selected.
   * @param {expression=} badge The badge to put on this tab (usually a number).
   * @param {expression=} badge-style The style of badge to put on this tab (eg: badge-positive).
   * @param {expression=} on-select Called when this tab is selected.
   * @param {expression=} on-deselect Called when this tab is deselected.
   * @param {expression=} ng-click By default, the tab will be selected on click. If ngClick is set, it will not.  You can explicitly switch tabs using {@link ionic.service:$ionicTabsDelegate#select $ionicTabsDelegate.select()}.
   * @param {expression=} hidden Whether the tab is to be hidden or not.
   * @param {expression=} disabled Whether the tab is to be disabled or not.
   */
  IonicModule
  .directive('ionTab', [
    '$compile',
    '$ionicConfig',
    '$ionicBind',
    '$ionicViewSwitcher',
    function ($compile, $ionicConfig, $ionicBind, $ionicViewSwitcher) {

      //Returns ' key="value"' if value exists
      function attrStr(k, v) {
        return isDefined(v) ? ' ' + k + '="' + v + '"' : '';
      }

      return {
        restrict: 'E',
        require: ['^ionTabs', 'ionTab'],
        controller: '$ionicTab',
        scope: true,
        compile: function (element, attr) {

          //We create the tabNavTemplate in the compile phase so that the
          //attributes we pass down won't be interpolated yet - we want
          //to pass down the 'raw' versions of the attributes
          var tabNavTemplate = '<ion-tab-nav' +
            attrStr('ng-click', attr.ngClick) +
            attrStr('title', attr.title) +
            attrStr('icon', attr.icon) +
            attrStr('icon-on', attr.iconOn) +
            attrStr('icon-off', attr.iconOff) +
            attrStr('badge', attr.badge) +
            attrStr('badge-style', attr.badgeStyle) +
            attrStr('hidden', attr.hidden) +
            attrStr('disabled', attr.disabled) +
            attrStr('class', attr['class']) +
            '></ion-tab-nav>';

          //Remove the contents of the element so we can compile them later, if tab is selected
          var tabContentEle = document.createElement('div');
          for (var x = 0; x < element[0].children.length; x++) {
            tabContentEle.appendChild(element[0].children[x].cloneNode(true));
          }
          var childElementCount = tabContentEle.childElementCount;
          element.empty();

          var navViewName, isNavView;
          if (childElementCount) {
            if (tabContentEle.children[0].tagName === 'ION-NAV-VIEW') {
              // get the name if it's a nav-view
              navViewName = tabContentEle.children[0].getAttribute('name');
              tabContentEle.children[0].classList.add('view-container');
              isNavView = true;
            }
            if (childElementCount === 1) {
              // make the 1 child element the primary tab content container
              tabContentEle = tabContentEle.children[0];
            }
            if (!isNavView) tabContentEle.classList.add('pane');
            tabContentEle.classList.add('tab-content');
          }

          return function link($scope, $element, $attr, ctrls) {
            var childScope;
            var childElement;
            var tabsCtrl = ctrls[0];
            var tabCtrl = ctrls[1];
            var isTabContentAttached = false;
            $scope.$tabSelected = false;

            $ionicBind($scope, $attr, {
              onSelect: '&',
              onDeselect: '&',
              title: '@',
              uiSref: '@',
              href: '@'
            });

            tabsCtrl.add($scope);
            $scope.$on('$destroy', function () {
              if (!$scope.$tabsDestroy) {
                // if the containing ionTabs directive is being destroyed
                // then don't bother going through the controllers remove
                // method, since remove will reset the active tab as each tab
                // is being destroyed, causing unnecessary view loads and transitions
                tabsCtrl.remove($scope);
              }
              tabNavElement.isolateScope().$destroy();
              tabNavElement.remove();
              tabNavElement = tabContentEle = childElement = null;
            });

            //Remove title attribute so browser-tooltip does not apear
            $element[0].removeAttribute('title');

            if (navViewName) {
              tabCtrl.navViewName = $scope.navViewName = navViewName;
            }
            $scope.$on('$stateChangeSuccess', selectIfMatchesState);
            selectIfMatchesState();

            function selectIfMatchesState() {
              if (tabCtrl.tabMatchesState()) {
                tabsCtrl.select($scope, false);
              }
            }

            var tabNavElement = jqLite(tabNavTemplate);
            tabNavElement.data('$ionTabsController', tabsCtrl);
            tabNavElement.data('$ionTabController', tabCtrl);
            tabsCtrl.$tabsElement.append($compile(tabNavElement)($scope));


            function tabSelected(isSelected) {
              if (isSelected && childElementCount) {
                // this tab is being selected

                // check if the tab is already in the DOM
                // only do this if the tab has child elements
                if (!isTabContentAttached) {
                  // tab should be selected and is NOT in the DOM
                  // create a new scope and append it
                  childScope = $scope.$new();
                  childElement = jqLite(tabContentEle);
                  $ionicViewSwitcher.viewEleIsActive(childElement, true);
                  tabsCtrl.$element.append(childElement);
                  $compile(childElement)(childScope);
                  isTabContentAttached = true;
                }

                // remove the hide class so the tabs content shows up
                $ionicViewSwitcher.viewEleIsActive(childElement, true);

              } else if (isTabContentAttached && childElement) {
                // this tab should NOT be selected, and it is already in the DOM

                if ($ionicConfig.views.maxCache() > 0) {
                  // keep the tabs in the DOM, only css hide it
                  $ionicViewSwitcher.viewEleIsActive(childElement, false);

                } else {
                  // do not keep tabs in the DOM
                  destroyTab();
                }

              }
            }

            function destroyTab() {
              childScope && childScope.$destroy();
              isTabContentAttached && childElement && childElement.remove();
              tabContentEle.innerHTML = '';
              isTabContentAttached = childScope = childElement = null;
            }

            $scope.$watch('$tabSelected', tabSelected);

            $scope.$on('$ionicView.afterEnter', function () {
              $ionicViewSwitcher.viewEleIsActive(childElement, $scope.$tabSelected);
            });

            $scope.$on('$ionicView.clearCache', function () {
              if (!$scope.$tabSelected) {
                destroyTab();
              }
            });

          };
        }
      };
    }]);

  IonicModule
  .directive('ionTabNav', [function () {
    return {
      restrict: 'E',
      replace: true,
      require: ['^ionTabs', '^ionTab'],
      template:
        '<a ng-class="{\'has-badge\':badge, \'tab-hidden\':isHidden(), \'tab-item-active\': isTabActive()}" ' +
        ' ng-disabled="disabled()" class="tab-item">' +
        '<span class="badge {{badgeStyle}}" ng-if="badge">{{badge}}</span>' +
        '<i class="icon {{getIcon()}}" ng-if="getIcon()"></i>' +
        '<span class="tab-title" ng-bind-html="title"></span>' +
        '</a>',
      scope: {
        title: '@',
        icon: '@',
        iconOn: '@',
        iconOff: '@',
        badge: '=',
        hidden: '@',
        disabled: '&',
        badgeStyle: '@',
        'class': '@'
      },
      link: function ($scope, $element, $attrs, ctrls) {
        var tabsCtrl = ctrls[0],
          tabCtrl = ctrls[1];

        //Remove title attribute so browser-tooltip does not apear
        $element[0].removeAttribute('title');

        $scope.selectTab = function (e) {
          e.preventDefault();
          tabsCtrl.select(tabCtrl.$scope, true);
        };
        if (!$attrs.ngClick) {
          $element.on('click', function (event) {
            $scope.$apply(function () {
              $scope.selectTab(event);
            });
          });
        }

        $scope.isHidden = function () {
          if ($attrs.hidden === 'true' || $attrs.hidden === true) return true;
          return false;
        };

        $scope.getIconOn = function () {
          return $scope.iconOn || $scope.icon;
        };
        $scope.getIconOff = function () {
          return $scope.iconOff || $scope.icon;
        };

        $scope.isTabActive = function () {
          return tabsCtrl.selectedTab() === tabCtrl.$scope;
        };

        $scope.getIcon = function () {
          if (tabsCtrl.selectedTab() === tabCtrl.$scope) {
            // active
            return $scope.iconOn || $scope.icon;
          } else {
            // inactive
            return $scope.iconOff || $scope.icon;
          }
        };
      }
    };
  }]);

  /**
   * @ngdoc directive
   * @name ionTabs
   * @module ionic
   * @delegate ionic.service:$ionicTabsDelegate
   * @restrict E
   * @codepen odqCz
   *
   * @description
   * Powers a multi-tabbed interface with a Tab Bar and a set of "pages" that can be tabbed
   * through.
   *
   * Assign any [tabs class](/docs/components#tabs) to the element to define
   * its look and feel.
   *
   * For iOS, tabs will appear at the bottom of the screen. For Android, tabs will be at the top
   * of the screen, below the nav-bar. This follows each OS's design specification, but can be
   * configured with the {@link ionic.provider:$ionicConfigProvider}.
   *
   * See the {@link ionic.directive:ionTab} directive's documentation for more details on
   * individual tabs.
   *
   * Note: do not place ion-tabs inside of an ion-content element; it has been known to cause a
   * certain CSS bug.
   *
   * @usage
   * ```html
   * <ion-tabs class="tabs-positive tabs-icon-top">
   *
   *   <ion-tab title="Home" icon-on="ion-ios-filing" icon-off="ion-ios-filing-outline">
   *     <!-- Tab 1 content -->
   *   </ion-tab>
   *
   *   <ion-tab title="About" icon-on="ion-ios-clock" icon-off="ion-ios-clock-outline">
   *     <!-- Tab 2 content -->
   *   </ion-tab>
   *
   *   <ion-tab title="Settings" icon-on="ion-ios-gear" icon-off="ion-ios-gear-outline">
   *     <!-- Tab 3 content -->
   *   </ion-tab>
   *
   * </ion-tabs>
   * ```
   *
   * @param {string=} delegate-handle The handle used to identify these tabs
   * with {@link ionic.service:$ionicTabsDelegate}.
   */

  IonicModule
  .directive('ionTabs', [
    '$ionicTabsDelegate',
    '$ionicConfig',
    function ($ionicTabsDelegate, $ionicConfig) {
      return {
        restrict: 'E',
        scope: true,
        controller: '$ionicTabs',
        compile: function (tElement) {
          //We cannot use regular transclude here because it breaks element.data()
          //inheritance on compile
          var innerElement = jqLite('<div class="tab-nav tabs">');
          innerElement.append(tElement.contents());

          tElement.append(innerElement)
          .addClass('tabs-' + $ionicConfig.tabs.position() + ' tabs-' + $ionicConfig.tabs.style());

          return {pre: prelink, post: postLink};

          function prelink($scope, $element, $attr, tabsCtrl) {
            var deregisterInstance = $ionicTabsDelegate._registerInstance(
              tabsCtrl, $attr.delegateHandle, tabsCtrl.hasActiveScope
            );

            tabsCtrl.$scope = $scope;
            tabsCtrl.$element = $element;
            tabsCtrl.$tabsElement = jqLite($element[0].querySelector('.tabs'));

            $scope.$watch(function () {
              return $element[0].className;
            }, function (value) {
              var isTabsTop = value.indexOf('tabs-top') !== -1;
              var isHidden = value.indexOf('tabs-item-hide') !== -1;
              $scope.$hasTabs = !isTabsTop && !isHidden;
              $scope.$hasTabsTop = isTabsTop && !isHidden;
              $scope.$emit('$ionicTabs.top', $scope.$hasTabsTop);
            });

            function emitLifecycleEvent(ev, data) {
              ev.stopPropagation();
              var previousSelectedTab = tabsCtrl.previousSelectedTab();
              if (previousSelectedTab) {
                previousSelectedTab.$broadcast(ev.name.replace('NavView', 'Tabs'), data);
              }
            }

            $scope.$on('$ionicNavView.beforeLeave', emitLifecycleEvent);
            $scope.$on('$ionicNavView.afterLeave', emitLifecycleEvent);
            $scope.$on('$ionicNavView.leave', emitLifecycleEvent);

            $scope.$on('$destroy', function () {
              // variable to inform child tabs that they're all being blown away
              // used so that while destorying an individual tab, each one
              // doesn't select the next tab as the active one, which causes unnecessary
              // loading of tab views when each will eventually all go away anyway
              $scope.$tabsDestroy = true;
              deregisterInstance();
              tabsCtrl.$tabsElement = tabsCtrl.$element = tabsCtrl.$scope = innerElement = null;
              delete $scope.$hasTabs;
              delete $scope.$hasTabsTop;
            });
          }

          function postLink($scope, $element, $attr, tabsCtrl) {
            if (!tabsCtrl.selectedTab()) {
              // all the tabs have been added
              // but one hasn't been selected yet
              tabsCtrl.select(0);
            }
          }
        }
      };
    }]);

  /**
   * @ngdoc directive
   * @name ionTitle
   * @module ionic
   * @restrict E
   *
   * Used for titles in header and nav bars. New in 1.2
   *
   * Identical to <div class="title"> but with future compatibility for Ionic 2
   *
   * @usage
   *
   * ```html
   * <ion-nav-bar>
   *   <ion-title>Hello</ion-title>
   * <ion-nav-bar>
   * ```
   */
  IonicModule
  .directive('ionTitle', [function () {
    return {
      restrict: 'E',
      compile: function (element) {
        element.addClass('title');
      }
    };
  }]);

  /**
   * @ngdoc directive
   * @name ionToggle
   * @module ionic
   * @codepen tfAzj
   * @restrict E
   *
   * @description
   * A toggle is an animated switch which binds a given model to a boolean.
   *
   * Allows dragging of the switch's nub.
   *
   * The toggle behaves like any [AngularJS checkbox](http://docs.angularjs.org/api/ng/input/input[checkbox]) otherwise.
   *
   * @param toggle-class {string=} Sets the CSS class on the inner `label.toggle` element created by the directive.
   *
   * @usage
   * Below is an example of a toggle directive which is wired up to the `airplaneMode` model
   * and has the `toggle-calm` CSS class assigned to the inner element.
   *
   * ```html
   * <ion-toggle ng-model="airplaneMode" toggle-class="toggle-calm">Airplane Mode</ion-toggle>
   * ```
   */
  IonicModule
  .directive('ionToggle', [
    '$timeout',
    '$ionicConfig',
    function ($timeout, $ionicConfig) {

      return {
        restrict: 'E',
        replace: true,
        require: '?ngModel',
        transclude: true,
        template:
          '<div class="item item-toggle">' +
          '<div ng-transclude></div>' +
          '<label class="toggle">' +
          '<input type="checkbox">' +
          '<div class="track">' +
          '<div class="handle"></div>' +
          '</div>' +
          '</label>' +
          '</div>',

        compile: function (element, attr) {
          var input = element.find('input');
          forEach({
            'name': attr.name,
            'ng-value': attr.ngValue,
            'ng-model': attr.ngModel,
            'ng-checked': attr.ngChecked,
            'ng-disabled': attr.ngDisabled,
            'ng-true-value': attr.ngTrueValue,
            'ng-false-value': attr.ngFalseValue,
            'ng-change': attr.ngChange,
            'ng-required': attr.ngRequired,
            'required': attr.required
          }, function (value, name) {
            if (isDefined(value)) {
              input.attr(name, value);
            }
          });

          if (attr.toggleClass) {
            element[0].getElementsByTagName('label')[0].classList.add(attr.toggleClass);
          }

          element.addClass('toggle-' + $ionicConfig.form.toggle());

          return function ($scope, $element) {
            var el = $element[0].getElementsByTagName('label')[0];
            var checkbox = el.children[0];
            var track = el.children[1];
            var handle = track.children[0];

            var ngModelController = jqLite(checkbox).controller('ngModel');

            $scope.toggle = new ionic.views.Toggle({
              el: el,
              track: track,
              checkbox: checkbox,
              handle: handle,
              onChange: function () {
                if (ngModelController) {
                  ngModelController.$setViewValue(checkbox.checked);
                  $scope.$apply();
                }
              }
            });

            $scope.$on('$destroy', function () {
              $scope.toggle.destroy();
            });
          };
        }

      };
    }]);

  /**
   * @ngdoc directive
   * @name ionView
   * @module ionic
   * @restrict E
   * @parent ionNavView
   *
   * @description
   * A container for view content and any navigational and header bar information. When a view
   * enters and exits its parent {@link ionic.directive:ionNavView}, the view also emits view
   * information, such as its title, whether the back button should be displayed or not, whether the
   * corresponding {@link ionic.directive:ionNavBar} should be displayed or not, which transition the view
   * should use to animate, and which direction to animate.
   *
   * *Views are cached to improve performance.* When a view is navigated away from, its element is
   * left in the DOM, and its scope is disconnected from the `$watch` cycle. When navigating to a
   * view that is already cached, its scope is reconnected, and the existing element, which was
   * left in the DOM, becomes active again. This can be disabled, or the maximum number of cached
   * views changed in {@link ionic.provider:$ionicConfigProvider}, in the view's `$state` configuration, or
   * as an attribute on the view itself (see below).
   *
   * @usage
   * Below is an example where our page will load with a {@link ionic.directive:ionNavBar} containing
   * "My Page" as the title.
   *
   * ```html
   * <ion-nav-bar></ion-nav-bar>
   * <ion-nav-view>
   *   <ion-view view-title="My Page">
   *     <ion-content>
   *       Hello!
   *     </ion-content>
   *   </ion-view>
   * </ion-nav-view>
   * ```
   *
   * ## View LifeCycle and Events
   *
   * Views can be cached, which means ***controllers normally only load once***, which may
   * affect your controller logic. To know when a view has entered or left, events
   * have been added that are emitted from the view's scope. These events also
   * contain data about the view, such as the title and whether the back button should
   * show. Also contained is transition data, such as the transition type and
   * direction that will be or was used.
   *
   * Life cycle events are emitted upwards from the transitioning view's scope. In some cases, it is
   * desirable for a child/nested view to be notified of the event.
   * For this use case, `$ionicParentView` life cycle events are broadcast downwards.
   *
   * <table class="table">
   *  <tr>
   *   <td><code>$ionicView.loaded</code></td>
   *   <td>The view has loaded. This event only happens once per
   * view being created and added to the DOM. If a view leaves but is cached,
   * then this event will not fire again on a subsequent viewing. The loaded event
   * is good place to put your setup code for the view; however, it is not the
   * recommended event to listen to when a view becomes active.</td>
   *  </tr>
   *  <tr>
   *   <td><code>$ionicView.enter</code></td>
   *   <td>The view has fully entered and is now the active view.
   * This event will fire, whether it was the first load or a cached view.</td>
   *  </tr>
   *  <tr>
   *   <td><code>$ionicView.leave</code></td>
   *   <td>The view has finished leaving and is no longer the
   * active view. This event will fire, whether it is cached or destroyed.</td>
   *  </tr>
   *  <tr>
   *   <td><code>$ionicView.beforeEnter</code></td>
   *   <td>The view is about to enter and become the active view.</td>
   *  </tr>
   *  <tr>
   *   <td><code>$ionicView.beforeLeave</code></td>
   *   <td>The view is about to leave and no longer be the active view.</td>
   *  </tr>
   *  <tr>
   *   <td><code>$ionicView.afterEnter</code></td>
   *   <td>The view has fully entered and is now the active view.</td>
   *  </tr>
   *  <tr>
   *   <td><code>$ionicView.afterLeave</code></td>
   *   <td>The view has finished leaving and is no longer the active view.</td>
   *  </tr>
   *  <tr>
   *   <td><code>$ionicView.unloaded</code></td>
   *   <td>The view's controller has been destroyed and its element has been
   * removed from the DOM.</td>
   *  </tr>
   *  <tr>
   *   <td><code>$ionicParentView.enter</code></td>
   *   <td>The parent view has fully entered and is now the active view.
   * This event will fire, whether it was the first load or a cached view.</td>
   *  </tr>
   *  <tr>
   *   <td><code>$ionicParentView.leave</code></td>
   *   <td>The parent view has finished leaving and is no longer the
   * active view. This event will fire, whether it is cached or destroyed.</td>
   *  </tr>
   *  <tr>
   *   <td><code>$ionicParentView.beforeEnter</code></td>
   *   <td>The parent view is about to enter and become the active view.</td>
   *  </tr>
   *  <tr>
   *   <td><code>$ionicParentView.beforeLeave</code></td>
   *   <td>The parent view is about to leave and no longer be the active view.</td>
   *  </tr>
   *  <tr>
   *   <td><code>$ionicParentView.afterEnter</code></td>
   *   <td>The parent view has fully entered and is now the active view.</td>
   *  </tr>
   *  <tr>
   *   <td><code>$ionicParentView.afterLeave</code></td>
   *   <td>The parent view has finished leaving and is no longer the active view.</td>
   *  </tr>
   * </table>
   *
   * ## LifeCycle Event Usage
   *
   * Below is an example of how to listen to life cycle events and
   * access state parameter data
   *
   * ```js
   * $scope.$on("$ionicView.beforeEnter", function(event, data){
   *    // handle event
   *    console.log("State Params: ", data.stateParams);
   * });
   *
   * $scope.$on("$ionicView.enter", function(event, data){
   *    // handle event
   *    console.log("State Params: ", data.stateParams);
   * });
   *
   * $scope.$on("$ionicView.afterEnter", function(event, data){
   *    // handle event
   *    console.log("State Params: ", data.stateParams);
   * });
   * ```
   *
   * ## Caching
   *
   * Caching can be disabled and enabled in multiple ways. By default, Ionic will
   * cache a maximum of 10 views. You can optionally choose to disable caching at
   * either an individual view basis, or by global configuration. Please see the
   * _Caching_ section in {@link ionic.directive:ionNavView} for more info.
   *
   * @param {string=} view-title A text-only title to display on the parent {@link ionic.directive:ionNavBar}.
   * For an HTML title, such as an image, see {@link ionic.directive:ionNavTitle} instead.
   * @param {boolean=} cache-view If this view should be allowed to be cached or not.
   * Please see the _Caching_ section in {@link ionic.directive:ionNavView} for
   * more info. Default `true`
   * @param {boolean=} can-swipe-back If this view should be allowed to use the swipe to go back gesture or not.
   * This does not enable the swipe to go back feature if it is not available for the platform it's running
   * from, or there isn't a previous view. Default `true`
   * @param {boolean=} hide-back-button Whether to hide the back button on the parent
   * {@link ionic.directive:ionNavBar} by default.
   * @param {boolean=} hide-nav-bar Whether to hide the parent
   * {@link ionic.directive:ionNavBar} by default.
   */
  IonicModule
  .directive('ionView', function () {
    return {
      restrict: 'EA',
      priority: 1000,
      controller: '$ionicView',
      compile: function (tElement) {
        tElement.addClass('pane');
        tElement[0].removeAttribute('title');
        return function link($scope, $element, $attrs, viewCtrl) {
          viewCtrl.init();
        };
      }
    };
  });

})();
